Java多线程之Thread

136 阅读9分钟

Java多线程之Thread

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Java中谈起多线程,一定不可能跳过Thread类,因为它是Java多线程的基石,不管你是使用线程池来实现多线程,还是通过实现Runnable接口来实现多线程,底层都一定经过了Thread,本文将简要介绍Java中Thread类的相关知识,也欢迎大家补充。

为什么说只有一种实现多线程的方法

一般问道Java中有几种实现多线程的方式这个问题时,脑海里都会想到以下几种:

  • 继承Thread类,重写run方法
  • 实现Runnable接口,覆写run方法
  • 实现Callable接口,传入线程池
  • 线程池...

那为什么说只有一种实现多线程的方法呢?

因为不管上面的哪一种方法实现多线程,其本质都是Thread类启动start方法开启一个线程,然后该线程执行Thread类中的run方法(异步任务)。

继承Thread类,重写run方法

public class ThreadTest {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        // 调用start方法,开启多线程
        myThread.start();
    }
​
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("通过继承Thread类,重写run方法开启多线程");
        }
    }
}

实现Runnable接口

public class ThreadTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyAsyncTask());
        // 调用start方法,开启多线程 
        thread.start();
        
        
        Runnable r = () -> System.out.println("lambda表达式实现Runnable接口");
        Thread thread1 = new Thread(r);
        // 调用start方法,开启多线程
        thread1.start();
    }
​
    static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("通过继承Thread类,重写run方法开启多线程");
        }
    }
    static class MyAsyncTask implements Runnable{
        /**
         * When an object implementing interface <code>Runnable</code> is used
         * to create a thread, starting the thread causes the object's
         * <code>run</code> method to be called in that separately executing
         * thread.
         * <p>
         * The general contract of the method <code>run</code> is that it may
         * take any action whatsoever.
         *
         * @see Thread#run()
         */
        @Override
        public void run() {
            System.out.println("实现Runnable接口");
        }
    }
}

总结

Java中新开启的线程的程序执行入口就是Thread类中的run方法,执行完run方法后,线程就会进入销毁阶段。

Thread类中的run方法如下:

private Runnable target;
​
public void run() {
    if (target != null) {
        target.run();
    }
}

当我们以继承Thread类,重写run方法的方式开启多线程时,开启的新线程就会执行我们重写的那个run方法;

当我们以实现Runnable接口,然后将该实现类传入Thread类中开启多线程时,开启的新线程就会执行Thread类中的run方法,而该方法会转而去执行Runnable接口中的run方法。

所以说只有一种实现多线程的方法,即通过Thread类的start方法开启多线程,

并且将任务交给异步线程的方法只有一种,即Thread类的run方法,只是有两种形式,一种是传入Runnable接口的实现类(即任务),一种是直接重写Thread类的run方法。

线程状态如何流转?

在Java中,线程有如下几种状态:

New

新建,表示线程被创建但尚未启动的状态:当我们用 new Thread() 新建一个线程时,如果线程没有开始运行 start() 方法,那么此时它的状态就是 New。

Runnable

线程调用了 start(),它的状态就会从 New 变成 Runnable,处于Runnable状态的线程在Java虚拟机中运行,也有可能在等待CPU分配资源。所以,如果一个正在运行的线程是 Runnable 状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是 Runnable,因为它有可能随时被调度回来继续执行任务。

Blocked

阻塞状态,处于Blocked状态的线程正等待锁的释放以进入同步区。从 Runnable 状态进入 Blocked 状态只有一种可能,就是进入 synchronized 保护的代码时没有抢到 monitor 锁,无论是进入 synchronized 代码块,还是 synchronized 方法,都是一样。

Waiting

等待状态。处于等待状态的线程变成Runnable状态需要其他线程唤醒。

调用如下3个方法会使线程进入等待状态:

  • Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
  • Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;
  • LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

Blocked 仅仅针对 synchronized monitor 锁,可是在 Java 中还有很多其他的锁,比如 ReentrantLock,如果线程在获取这种锁时没有抢到该锁就会进入 Waiting 状态,因为本质上它执行了 LockSupport.park() 方法,所以会进入 Waiting 状态。同样,Object.wait() 和 Thread.join() 也会让线程进入 Waiting 状态。

Blocked 与 Waiting 的区别是 Blocked 在等待其他线程释放 monitor 锁,而 Waiting 则是在等待某个条件,比如 join 的线程执行完毕,或者是 notify()/notifyAll() 。

Timed Waiting

超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。

调用如下方法会使线程进入超时等待状态:

  • Thread.sleep(long millis):使当前线程睡眠指定时间;
  • Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
  • Thread.join(long millis):等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;
  • LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
  • LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;

Terminated

终止状态,要想进入这个状态有两种可能。

  • run() 方法执行完毕,线程正常退出。
  • 出现一个没有捕获的异常,终止了 run() 方法,最终导致意外终止。

状态流转

注意点:

  1. 线程的状态是需要按照箭头方向来走的,比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。
  2. 线程生命周期不可逆:一旦进入 Runnable 状态就不能回到 New 状态;一旦被终止就不可能再有任何状态的变化。所以一个线程只能有一次 New 和 Terminated 状态,只有处于中间状态才可以相互转换。

线程间如何通信?

  • 线程间如何通信?即:线程之间以何种机制来交换信息
  • 线程间如何同步?即:线程以何种机制来控制不同线程间操作发生的相对顺序

有两种并发模型可以解决这两个问题:

  • 消息传递并发模型
  • 共享内存并发模型

这两种模型之间的区别如下表所示:

两种并发模型的比较

在Java中,使用的是共享内存并发模型。

锁与同步

同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication) :

  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。 换句话说,就是由调用者主动等待这个调用的结果
  • 而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态:

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

线程同步是线程之间按照一定的顺序执行。

可以通过锁来限制不同线程之间的执行顺序。

代码示例:

public class ObjectLock {
    private static Object lock = new Object();
​
    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread A " + i);
                }
            }
        }
    }
​
    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Thread B " + i);
                }
            }
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(10);
        new Thread(new ThreadB()).start();
    }
}

这里声明了一个名字为lock的对象锁。我们在ThreadAThreadB内需要同步的代码块里,都是用synchronized关键字加上了同一个对象锁lock

上文我们说到了,根据线程和锁的关系,同一时间只有一个线程持有一个锁,那么线程B就会等线程A执行完成后释放lock,线程B才能获得锁lock

这里在主线程里使用sleep方法睡眠了10毫秒,是为了防止线程B先得到锁。因为如果同时start,线程A和线程B都是出于就绪状态,操作系统可能会先让B运行。这样就会先输出B的内容,然后B执行完成之后自动释放锁,线程A再执行。

等待-通知机制

Java多线程的等待/通知机制是基于Object类的wait()方法和notify(), notifyAll()方法来实现的。

notify()方法会随机叫醒一个正在等待的线程,而notifyAll()会叫醒所有正在等待的线程。

前面我们讲到,一个锁同一时刻只能被一个线程持有。而假如线程A现在持有了一个锁lock并开始执行,它可以使用lock.wait()让自己进入等待状态。这个时候,lock这个锁是被释放了的。

这时,线程B获得了lock这个锁并开始执行,它可以在某一时刻,使用lock.notify(),通知之前持有lock锁并进入等待状态的线程A,说“线程A你不用等了,可以往下执行了”。

需要注意的是,这个时候线程B并没有释放锁lock,除非线程B这个时候使用lock.wait()释放锁,或者线程B执行结束自行释放锁,线程A才能得到lock锁。

代码示例:

public class WaitAndNotify {
    private static Object lock = new Object();
​
    static class ThreadA implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadA: " + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }
​
    static class ThreadB implements Runnable {
        @Override
        public void run() {
            synchronized (lock) {
                for (int i = 0; i < 5; i++) {
                    try {
                        System.out.println("ThreadB: " + i);
                        lock.notify();
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                lock.notify();
            }
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}
​
// 输出:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
ThreadA: 3
ThreadB: 3
ThreadA: 4
ThreadB: 4

在这个Demo里,线程A和线程B首先打印出自己需要的东西,然后使用notify()方法叫醒另一个正在等待的线程,然后自己使用wait()方法陷入等待并释放lock锁。

需要注意的是等待/通知机制使用的是使用同一个对象锁,如果你两个线程使用的是不同的对象锁,那它们之间是不能用等待/通知机制通信的。

volatile

volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的。

比如我现在有一个需求,我想让线程A输出0,然后线程B输出1,再然后线程A输出2…以此类推。我应该怎样实现呢?

代码:

public class Signal {
    private static volatile int signal = 0;
​
    static class ThreadA implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 0) {
                    System.out.println("threadA: " + signal);
                    signal++;
                }
            }
        }
    }
​
    static class ThreadB implements Runnable {
        @Override
        public void run() {
            while (signal < 5) {
                if (signal % 2 == 1) {
                    System.out.println("threadB: " + signal);
                    signal = signal + 1;
                }
            }
        }
    }
​
    public static void main(String[] args) throws InterruptedException {
        new Thread(new ThreadA()).start();
        Thread.sleep(1000);
        new Thread(new ThreadB()).start();
    }
}
​
// 输出:
threadA: 0
threadB: 1
threadA: 2
threadB: 3
threadA: 4

我们可以看到,使用了一个volatile变量signal来实现了“信号量”的模型。这里需要注意的是,volatile变量需要进行原子操作。

管道通信

基本不用。

内容来源

5 Java线程间的通信 · 深入浅出Java多线程 (redspider.group)

03 线程是如何在 6 种状态之间转换的?.md (lianglianglee.com)

01 为何说只有 1 种实现线程的方法?.md (lianglianglee.com)