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() 方法,最终导致意外终止。
状态流转
注意点:
- 线程的状态是需要按照箭头方向来走的,比如线程从 New 状态是不可以直接进入 Blocked 状态的,它需要先经历 Runnable 状态。
- 线程生命周期不可逆:一旦进入 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的对象锁。我们在ThreadA和ThreadB内需要同步的代码块里,都是用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)