欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java多线程快速入门

程序员文章站 2022-05-05 09:39:01
...

本篇主要粗略地写了Java多线程的简单使用,由于篇幅有限,故没能做出更深入的介绍,和源码的解释,之后更多更详细的介绍会在之后更新在下面的文章中:
Java 多线程与并发编程(持续更新):https://blog.csdn.net/weixin_39778570/article/details/94998437

概要

Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务

多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

这里定义和线程相关的另一个术语 - 进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

多线程能满足程序员编写高效率的程序来达到充分利用 CPU 的目的。

线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。
Java多线程快速入门

  • 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
    下图显示了一个线程完整的生命周期。在一个线程调用start方法之前,他就是new状态.

  • 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

  • 运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

  • 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
    1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
    2.同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
    3.其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  • 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台

线程的创建

Java 提供了三种创建线程的方法:

1:通过实现 Runnable 接口;
2:通过继承 Thread 类本身;
3:通过 Callable 和 Future 创建线程。
  • 继承Thread类
    继承Thread类,然后重新run方法,直接调用start方法即可。
class ThreadDemo extends Thread{
    String threadName;
    public ThreadDemo(String threadName){
        this.threadName = threadName;
    }
    // 重写run方法,作为线程的执行单元
    public void run(){
        System.out.println("Running: "+threadName);
        try{
            for (int i = 1; i < 6; i++) {
                System.out.println(threadName +" -> "+ i);
                Thread.sleep(100);
            }
        }catch(InterruptedException e){
            System.out.println("Thread "+threadName +" is interrupted.");
        }
        System.out.println("Thread " + threadName +" ending.");
    }
}
  • 通过实现Runnable接口
    通过实现Runnable接口,然后重写start方法,与继承Thread想相比,这种方法可以继承其他类,实现多重继承
class RunnableDemo implements Runnable{
    private String threadName;
    private Thread thread;
    public RunnableDemo(String threadName){
        this.threadName = threadName;
    }
    @Override
    public void run() {
        System.out.println("Running: "+threadName);
        try {
            for (int i = 0; i < 6; i++) {
                System.out.println(threadName +" -> "+ i);
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            System.out.println("Thread "+threadName +" is interrupted.");
        }
        System.out.println("Thread " + threadName +" ending.");
    }

    // 自己定义的start方法,使用本对象作为Thread的参数,调用Thread的start方法
    public void start(){
        if(thread == null){
            thread = new Thread(this,threadName);
            thread.start();
        }
    }
}
  • 通过 Callable 和 Future 创建线程
    当我们需要线程运行的时候需要有返回值的时候,我们可以考虑采取这种方式
  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回 值。
  2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封 装了该 Callable 对象的 call() 方法的返回值。
  3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
  4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
class CallableThreadTest implements Callable<Integer> {
    public void start(){
        CallableThreadTest ctt = new CallableThreadTest();
        FutureTask<Integer> ft = new FutureTask<Integer>(ctt);
        for(int i = 0;i < 100;i++) {
            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
            if(i==20) {
                new Thread(ft,"有返回值的线程").start();
            }
        }
        try {
            System.out.println("子线程的返回值:"+ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    // 重写call方法
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for(;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }
}
  • 调用
public class 创建线程 {
    public static void main(String[] args) {
 		  // 使用继承实现的线程
//        ThreadDemo t1 = new ThreadDemo("Thread-1");
//        ThreadDemo t2 = new ThreadDemo("Thread-2");
//        t1.start();
//        t2.start();
		  // 使用实现接实现的线程 
//        RunnableDemo runnableDemo1 = new RunnableDemo("Thread-3");
//        RunnableDemo runnableDemo2 = new RunnableDemo("Thread-4");
//        runnableDemo1.start();
//        runnableDemo2.start();
		// 使用cell的方式
        CallableThreadTest callableThreadTest = new CallableThreadTest();
        callableThreadTest.start();
    }
}

运行结果
Java多线程快速入门

数据共享

当我们采用普通方法时候

class TicketWindow extends Thread{
    private int index;
    private static final int MAX = 10;
    private String windowNmae;
    public TicketWindow(String windowNmae){
        this.windowNmae = windowNmae;
    }
    public void run(){
        while(index<=MAX){
            System.out.println(windowNmae+"出售票:"+index++);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class 数据共享与线程同步 {
    public static void main(String[] args) {
        TicketWindow ticketWindow1 = new TicketWindow("窗口1");
        TicketWindow ticketWindow2 = new TicketWindow("窗口2");
        TicketWindow ticketWindow3 = new TicketWindow("窗口3");
        ticketWindow1.start();
        ticketWindow2.start();
        ticketWindow3.start();
    }
}

运行结果:可以看到运行结果同一张票被出售了多次
Java多线程快速入门
为了使得票能够在多个线程中实现数据共享,于是我们采用了static和实现runnable接口两种方式

// 对index进行了static修饰实现变量的共享
class TicketWindow1 extends Thread{
    static HashMap mp = new HashMap<Integer,Boolean>();
    private static int index;
    private static final int MAX = 500;
    private String windowNmae;
    public TicketWindow1(String windowNmae){
        this.windowNmae = windowNmae;
    }
    public void run(){
        while(index<=MAX){
            if (mp.containsKey(index))
                System.out.println("----------------冲突了,编号:"+index);
            else mp.put(index,true);
            System.out.println(windowNmae+"出售票:"+index);
            index++;
        }
    }
}

// 使用static进行数据共享的时候只实现是部分数据的共享
// 但是我们要实现很多数据的共享呢,而且static修饰之后对象的生命周期会变得很长
// 对此,我们使用实现runnable接口实现数据共享

class TicketWindow2 implements Runnable{
    private int max = 10;
    private int index = 0;

    @Override
    public void run() {
       while(index<=max){
           System.out.println("出售票:"+index++);
       }
    }
}
public class 数据共享与线程同步 {
    public static void main(String[] args) {

        for (int i=1; i<=50; i++)
            new TicketWindow1("窗口"+i).start();
        // 可以看到在小数据的时候基本是不存在冲突的,也就是说每张票只被出售了一次
        // 但是数据(MAX)变大的时候,仍然存在同张票被出售2次了,因为可能同时两个值相同的index进入到运算栈里

//        TicketWindow2 t = new TicketWindow2();
//        Thread thread1 = new Thread(t);
//        Thread thread2 = new Thread(t);
//        thread1.start();
//        thread2.start();
        // 可以看到在小数据的时候基本是不存在冲突的,也就是说每张票只被出售了一次
        // 可惜的是,当数据变大的时候,这种方式和static一样,会出现大量数字未被使用或者使用多次的情况
        // 所以说这两种方式是线程不安全的
    }
}

运行结果:这两种方式都是可以实现数据共享的,但是遗憾的是,他们都无法做到数据同步,也就是线程不安全了。那么我们接下来我们就讨论如何让数据同步。
Java多线程快速入门

线程同步

对多线程共享的数据称为临界资源或同步资源
而把访问同步资源的那部分代码称为临界资源或临界区
为了确保临界区只被一个线程执行,Java采用了互斥锁的机制
当一个线程获得互斥锁的时候,其他线程就不能捕获得该锁,只能等该线程释放后再获得
这与之前并发交替执行代码的方式是不同的,这是一种串行的方式

这里的例子,是银行取款的简单样例,我们对take方法上锁了,每次只运行一个线程对该临界区进行访问,保证了里面的数据对多个线程来说是同步的。

class Mbank{
    // 共享数据
    private static int sum = 2000;
    public synchronized static void take(int k){
//    public static void take(int k){ // 如果采用这条语句的话sum同一个值会被使用多次,是不安全的
        int temp = sum;
        temp -= k;
        if(temp<=0){
            System.out.println("余额不足,请充值");
            return;
        }
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sum = temp;
        System.out.println("sum = " + sum);
    }
}
class Customer extends Thread{
    public void run(){
        for(int i=1; i<=500; i++)
            Mbank.take(100);
    }
}
public class 数据共享与线程同步2 {
    public static void main(String[] args) {
        Customer customer1 = new Customer();
        Customer customer2 = new Customer();
        customer1.start();
        customer2.start();
    }
}

运行结果:这是使用上面的上锁的方法执行的,如果使用不上锁将会出现数据错乱,读者可以打开注释自行尝试
Java多线程快速入门

线程之间的通信

线程的通信必须在synchronized锁定的区域内执行
实现两个线程,当一个线程存入一张票的时候,另一个线程取出一张票

class Tickets{
    protected int size;
    public int number = 0;
    boolean available = false;
    public Tickets(int size){
        this.size = size;
    }
    public synchronized void put(){
        if(available) { // 如果有票的话
            try {
                wait(); // 等待,进入阻塞状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("存入第: " + (++number) + " 号票");
        available = true; // 已经存入票了
        notify(); // 通知取票线程售票
    }
    public synchronized void sell(){
        if(!available) { // 如果没票的话
            try {
                wait();  // 等待,进入阻塞线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("售出第: " + (number) + " 号票");
        available = false;
        notify(); // 售票完毕后唤醒村票线程
        if(number==size)number = size+1; // 最多出售size张票
    }
}
// 存票线程
class Producer extends Thread{
    Tickets t = null;
    public Producer(Tickets t){
        this.t = t;
    }
    public void run(){
        while(t.number < t.size){
            t.put();
        }
    }
}
// 售票线程
class Consumer extends Thread{
    Tickets t = null;
    public Consumer(Tickets t){
        this.t = t;
    }
    public void run(){
        while(t.number <= t.size){
            t.sell();
        }
    }
}
public class 线程之间的通信 {
    public static void main(String[] args) {
        Tickets t = new Tickets(10);
        Consumer consumer = new Consumer(t);
        Producer producer = new Producer(t);
        consumer.start();
        producer.start();
    }
}

运行结果
Java多线程快速入门

附录

Thread类的一些重要方法

方法 描述
public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
public final void setName(String name) 改变线程名称,使之与参数 name 相同。
public final void setPriority(int priority) 更改线程的优先级。
public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
public void interrupt() 中断线程。
public final boolean isAlive() 测试线程是否处于活动状态。

Thread类的静态方法

方法 描述
public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。

小结

  1. 线程是指程序的运行流程。"多线程"的机制可以同时运行好几个程序块,使得程序的运行的效率更高,也可以可以传统程序语言无法解决的问题。
  2. 多线程与多任务是两个不同的概念,多任务是针对操作系统而言的,表示操作系统可以同时运行多个应用程序;而多线程是针对一个程序而言的,表示在一个程序内部可以同时执行多个线程。
  3. 创建线程有三种方法:传统的两种中,一种是继承java.lang的Thread类;另一种是用户在定义自己的类中实现Runnable接口。
  4. run()方法给出了线程要执行的任务。若是派生自Thread类,必须把线程的程序代码编写在run()方法内,实现覆盖操作;若是实现Runnable接口,必须在实现Runnable接口的类里定义run()方法。
  5. 每一个线程,在创建和消亡之前,均会处于下列五种状态之一:新建状态,就绪状态,运行状态,阻塞状态,消亡状态。
  6. 阻塞状态一般来自,该线程调用对象的wait()方法,调用sleep方法,和另外一个线程join()在一起,有优先级更高的线程处于就绪状态。
  7. 线程在运行时,因不需要外部的数据或方法,就不必关心其他线程的状态或行为,这样的线程称为独立,不同步或者异步执行的。
  8. 被多个线程共享的数据在同一时刻只允许一个线程处于操作之中,这就是线程控制中的线程间互斥。
  9. synchronized 锁定的是一个具体对象,通常是临界区对象。所有锁定同一个对象的线程之间在synchronized代码上是互斥的,也就是说这些线程的synchronized代码之间是串行执行的,不再是互相穿插并发执行,因为保证了synchronized代码块操作的原子性。
  10. 由于所有锁定同一个对象的线程之间,在synchronized代码块上是互斥的,这些线程的synchronized代码之间是串行执行的,故锁定的代码数量越好越好,否则多线程就会失去很多并发执行的优秀,而且synchronized是个比较重的操作。
  11. 一定要保证所有临界区共享的访问与操作均在syncgronized 代码块中执行。