- 1. 线程的基本概念
- 2. Java 线程初始化的两种方式
- 3. Thread 类的 API(等待/通知机制)
- 4. Java 正确终止线程的方法
- 参考
1. 线程的基本概念
1.1 线程的状态
- Java 线程的状态有六种:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
- 经典线程五态模型的状态有五种:创建、就绪、执行、阻塞、终止。
Java 将五态模型中的就绪和执行都统一成 RUNNABLE,将阻塞(即不可能得到 CPU 运行的机会)细分为了 BLOCKED、WAITING、TIMED_WAITNG,这里我们不评价好坏。也就是说,BLOCKED、WAITING、TIMED_WAITING 这几个状态,线程都不可能得到 CPU 的运行权,你叫它挂起、阻塞、睡眠、等待都可以。
1.1.1 NEW
当新建一个 Thread 类的对象,调用了 Thread 类的构造方法,那么就是创建了一个新的线程,此时这个线程的状态就是 NEW(初始态),但是并没有开始运行。但需要注意的是,此时并没有在操作系统层面创建一个线程。
在 Linux 操作系统中,是没有线程刚创建但没启动这种说法的。在操作系统层面,线程创建即刻开始运行。
1.1.2 RUNNABLE
调用 Thread 对象的 start() 方法之后,会调用一个本地方法 start0() 方法,最终会调用 JVM 中创建线程的方法 pthread_create() 方法。这时,在操作系统内核中,一个真正的线程才被创建出来。
- Thread 类调用 start() 后,操作系统中才真正出现了一个线程,并且立刻运行。
- Java 中的线程和操作系统中的线程,是一对一的关系。
- Thread 类调用 start() 后,线程状态变为 RUNNABLE,这是由本地方法中的部分代码造成的。
RUNNBALE 包括操作系统中的就绪和执行状态。CPU 的一个核心在同一时刻只能运行一个线程,具体执行哪个线程,要看操作系统的调度机制。所以,RUNNABLE 状态准确说是得到了可以随时准备运行的机会的状态。而处于这个状态中的线程,也分为就绪和执行两种状态:
- 正在 CPU 中运行的线程(执行)
-
1.1.3 TERMINATED
已终止线程的线程状态,线程已经结束执行。如果此时再强行执行 start(),将会报出错误。
1.1.4 BLOCKED
当线程等待进入同步区域时,会进入这种状态。下面是一个具体的例子:
public static void test0() {
// 创建一个对象lock
Object lock = new Object();
// 一个线程,执行一个 synchronized 块,锁对象是 lock,且一直持有这把锁不放
new Thread(() -> {
synchronized (lock) {
while (true) {
System.out.println("我是第一个线程");
}
}
}).start();
// 另一个线程,也同样执行一个锁对象为 lock 的 synchronized 块
new Thread(() -> {
synchronized (lock) {
while (true) {
System.out.println("我是第二个线程");
}
}
}).start();
}
对于第二个线程,在进入 synchronized 块时,因为无法拿到锁,线程状态会变为 BLOCKED,此时线程会进入一个该锁对象的同步队列。当持有锁的这个线程释放了锁之后,会唤醒该锁对象同步队列中的所有线程,这些线程会继续尝试抢锁,如此往复。同样,对于 synchronized 方法,也是如此。
比如,有一个锁对象 A,线程 1 此时持有这把锁。线程 2、3、4 分别尝试抢这把锁失败。
线程 1 释放锁,线程 2、3、4 重新变为 RUNNABLE,继续抢锁,假如此时线程 3 抢到了锁。
如此往复。
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
1.1.5 WAITING
有三大类方法可以使一个线程从 RUNNABLE 的状态转变为 WAITING 的状态,分别是 wait/notify/noifyAll、join、park/unpark。
1.1.5.1 wait/notify/notifyAll
public static void test0() {
// 创建一个对象lock
Object lock = new Object();
// lock.wait()方法调用时会释放锁对象、线程状态变成WAITING、线程进入锁对象的等待队列
new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
if (i == 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("我是第一个线程 i=" + i);
}
}
}).start();
// 另一个线程,调用同一个对象的 notify/notifyAll 方法
new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
if (i == 3) {
lock.notify();
}
System.out.println("我是第二个线程 i=" + i);
}
}
}).start();
}
当第一个线程调用 lock.wait()
时,第一个线程会发生三件事:
- 释放锁对象 lock
- 线程状态变成 WAITING
- 线程进入 lock 对象的等待队列(等待队列是 wait() 方法独有的)
什么时候这个线程被唤醒,从等待队列中移出,并从 WAITING 状态返回 RUNNABLE 状态呢?必须由另一个线程,调用同一个对象的 notify/notifyAll 方法。notify 是只唤醒一个线程,而 notifyAll 是唤醒所有等待队列中的线程。
需要注意,被唤醒后的线程,从等待队列移出,状态变为 RUNNABLE,但仍然需要抢锁,抢锁成功了,才可以从 wait 方法返回,继续执行。如果失败了,就和上一部分的 BLOCKED 流程一样了。
需要注意的是,wait/notify/noifyAll 方法必须搭配 synchronized 使用。
1.1.5.2 join
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是新创建的线程,刚刚睡了10s");
}
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是主线程");
}
当执行到 t.join()
的时候,主线程会变成 WAITING 状态,直到线程 t 执行完毕,主线程才会变回 RUNNABLE 状态,继续往下执行。看起来就像是主线程执行过程中,另一个线程插队加入(join),而且要等到其结束后主线程才继续。
Thread.join
的源码非常简单:
public synchronized void join() {
while (isAlive()) {
wait();
}
}
可以看到, join()
本质仍然是执行了 wait()
方法。主线程调用了 wait ,那么需要另一个线程 notify 才行。其实是 t 线程结束后,会由 JVM 自动调用 t.notifyAll()
。所以,其实 join()
就是 wait
,t 线程结束后会自动调用 notifyAll()
,然后主线程才会继续执行。
1.5.1.3 park/unpark
park/unpark 是 LockSupport 类的一个静态方法。
- 一个线程调用
LockSupport.park()
,该线程的状态会从 RUNNABLE 变成 WAITING。 - 另一个线程调用
LockSupport.unpark(Thread 刚刚的线程)
,刚刚的线程会从 WAITING 回到 RUNNABLE。
但从线程状态流转来看,park/unpark 与 wait 和 notify 相同。从实现机制上看,他们甚至更为简单:
- park 和 unpark 无需事先获取锁,或者说跟锁压根无关。
- 没有什么等待队列一说,unpark 会精准唤醒某一个确定的线程。
- park 和 unpark 没有顺序要求,可以先调用 unpark。
关于第三点,就涉及到 park 的原理了,这里我只简单说明。线程有一个计数器,初始值为 0。
- 调用 park:如果这个值为0,就将线程挂起,状态改为 WAITING。如果这个值为1,则将这个值改为0,其余的什么都不做。
- 调用 unpark:将这个值改为 1。
来看下面的例子:
public static void test1() {
// 例子1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
System.out.println("可以运行到这");
// 例子2
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
System.out.println("可以运行到这");
// 例子3
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.unpark(Thread.currentThread()); // 1
LockSupport.park(); // 0
LockSupport.park(); // WAITING
System.out.println("不可以运行到这");
}
park 的使用非常简单,同时也是 JDK 中锁实现的底层。JDK 中锁的实现是基于 AQS 的,而 AQS 的底层是用 park 和 unpark 来挂起和唤醒线程的。
1.1.6 TIMED_WAITING
这部分非常简单,将上面导致线程变成 WAITING 状态的那些方法,都增加一个超时参数,就变成了将线程变成 TIMED_WAITING 状态的方法了。
除了 wait()
、 join()
、 parkNanos(long)
、 parkUtil(long)
,还有一个方法能够仅仅让线程挂起,然后只能在等待时间超时的时候被唤醒,这个方法就是 Thread.sleep(long)
。
1.1.7 调用 Lock 接口中的 lock() 时,如果获取不到锁,线程的状态是什么?
可能许多人认为这时候应该和 synchronized 获取不到锁的效果一样,线程会变成 BLOCKED 状态。答案是否定的。
lock()
的实现是基于 AQS 的,而 AQS 的底层是用 park 和 unpark 来挂起和唤醒线程,所以这时候应该是变成 WAITING 或 TIME_WAITING 状态。
1.1.8 调用阻塞 IO 方法,线程变成什么状态?
比如 socket 编程时,调用如 accept()
, read()
这种阻塞方法时,线程处于什么状态呢?
答案是处于 RUNNABLE 状态,但实际上这个线程是得不到运行权的,因为在操作系统层面处于阻塞态,需要等到 IO 就绪,才能变为就绪态。这是因为在 Java 层面,JVM 认为等待 IO 和等待 CPU 执行是一样的。Java 就是这么设计的,这里不讨论其好坏。
1.2 线程的优先级
优先级代表线程执行的机会的大小,优先级高的可能先执行,低的可能后执行,在 Java 源码中,优先级从低到高分别是 1 到 10,线程默认 new 出来的优先级都是 5,源码如下:
// 最低优先级
public final static int MIN_PRIORITY = 1;
// 普通优先级,也是默认的
public final static int NORM_PRIORITY = 5;
// 最大优先级
public final static int MAX_PRIORITY = 10;
1.3 守护线程
我们默认创建的线程都是非守护线程。在 Java 中,当没有非守护线程存在时,JVM 就会结束自己的生命周期。而守护进程也会自动退出。守护线程一般用于执行独立的后台业务。比如 JAVA 的垃圾清理就是由守护线程执行。而所有非守护线程都退出了,也没有垃圾回收的需要了,所以守护线程就随着 JVM 一起关闭了。
使用 Thread 类的 setDaemon(true) 方法可以将线程设置为守护线程,不过需要在调用 start() 方法前调用这个方法,否则会抛出 IllegalThreadStateException 异常。
1.4 调用 Thread 类的 start() 和 run() 之间的区别
- start() 方法被用来启动新创建的线程,使该被创建的线程状态变为可运行状态。
- 而调用 run() 方法的时候,只会是在原来的线程中调用,没有新的线程启动,只有调用 start() 方法才会启动新线程。如果直接执行 run() 方法,会把 run() 方法当作一个 main 线程下的普通方法去执行,并不会在新的线程中执行它,这并不是多线程工作。
因此,为了在新的线程中执行我们的代码,必须使用 Thread.start() 方法。
1.5 sleep() 和 wait() 的区别
- wait 必须搭配 synchronize 一起使用,不然在运行时就会抛出 IllegalMonitorStateException 的异常,而 sleep 不需要。
- wait 方法属于 Object 类的方法,而 sleep 属于 Thread 类的方法。
- wait 方法不需要传递任何参数,表示永久休眠,进入 wait 状态的线程需要被 notify 和 notifyAll 线程唤醒;而 sleep 方法必须要传递一个超时时间的参数,且过了超时时间之后,线程会自动唤醒。
- wait 方法会主动地释放对象锁,但 sleep 方法不会;
- 在调用 wait 方法之后,线程会变为 WATING 状态;而调用 sleep 方法之后,线程会变为 TIMED_WAITING 状态。
2. Java 线程初始化的两种方式
2.1 Java 线程初始化的两种方式
在 Java 中实现多线程有两种方式:
- 继承 Thread 类
- 实现 Runnable 接口
继承 Thread 类和实现 Runnable 接口是 Java 实现多线程最基本的方式,它们都是没有返回值的,而 Callable 则解决了这个问题。
本质上,实现线程只有一种方式,就是构造一个 Thread 类。而要想实现线程执行的内容,却有两种方式,也就是可以通过实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行。无论是 Callable 还是 FutureTask,它们和 Runnable 一样,都是一个任务,是需要被执行的,而不是说它们本身就是线程。
2.2 Thread、Runnable、Callable 三者之间的区别
- Runnable 是实现 Runnable 接口,而 Thread 是继承 Thread 类。两者都需要实现 run () 方法,最后都要调用start ()方法。
- 实现 Runnable 接口的方法在启动时,需要作为参数传递给 Thread 类。
- Thread 实现了 Runnable,本身就实现了 Runnable 接口的 run() 方法,但同时负责线程创建、线程状态变更等操作。
- Runnable 是无返回值任务接口,Callable 是有返回值任务接口,如果任务需要跑起来,必须需要 Thread 的支持才行,Runnable 和 Callable 只是任务的定义,具体执行还需要靠 Thread。
Runnable 和 Callable 的主要区别是 Callable 的 call() 方法可以返回值和抛出异常,而 Runnable 的 run() 方法没有这些功能。Callable 可以返回装载有计算结果的 Future 对象。
2.3 为什么实现 Runnable 接口比继承 Thread 类实现线程更好?
Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明。
- Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。
2.4 继承 Thread 类,重写 run() 方法
```java class MyThread extends Thread { @Override public void run() { System.out.println(“testThread”); System.out.println(Thread.currentThread().getName());
} }log.info(Thread.currentThread().getName());
// 通过start()方法调用run()方法 @Test public void extendThreadInit() { System.out.println(Thread.currentThread().getName()); new MyThread().start(); }
上述代码打印出的线程名称是:Thread-0,而主线程的名字是:Thread [main,5,main],由此可见,的确是开了一个子线程来执行打印的操作。
我们一起来看下 start 的底层源码:
```java
public synchronized void start() {
// 检查线程的状态,是否可以启动
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 把线程加入线程group
group.add(this);
// 调用start0
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
从上面的代码可以看出,start 方法调用了 start0 方法,start0 方法在 JVM 中,start0 中的逻辑会调用 run 方法。其中,start0 是一个native方法,也称为 JNI(Java Native Interface)方法。JNI 方法是 Java 和其它语言交互的方式,即 Java 代码和虚拟机交互的方式,虚拟机就是由 C++ 和汇编所编写。
2.4 实现 Runnable 接口,作为 Thread 的入参
@Test
public void init() {
log.info("{} is run。", Thread.currentThread().getName());
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.info("{} begin run", Thread.currentThread().getName());
}
});
thread.start(); // 开一个子线程去执行
thread.run(); // 不会新起线程,是在当前主线程上继续运行
}
这种就是实现 Runnable 的接口,并作为 Thread 构造器的入参,我们调用时使用了两种方式,可以根据情况选择使用 start 或 run 方法,使用 start 会开启子线程来执行 run 里面的内容,使用 run 方法执行的还是主线程。
继续看看 run() 方法的源码。可以看到如果重写了 run() 方法,那么执行的是重写后的逻辑;如果没有重写,那么执行的是传入的 Runnable 的逻辑。
// 简单的运行,不会新起线程
// 这里的 target 就是在 new Thread 时,赋值的 Runnable。
public void run() {
if (target != null) {
target.run();
}
}
/* What will be run. */
private Runnable target;
2.5 使用 Callable 和 FutureTask
2.5.1 使用方法
分为以下几个步骤:
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
- 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
@Test public void testThreadByCallable() throws ExecutionException, InterruptedException { FutureTask futureTask = new FutureTask(new Callable<String>() { @Override public String call() throws Exception { Thread.sleep(3000); String result = "我是子线程" + Thread.currentThread().getName(); log.info("子线程正在运行:{}", Thread.currentThread().getName()); return result; } }); new Thread(futureTask).start(); log.info("返回的结果是 {}", futureTask.get()); }
Callable 是一个接口,约定了线程要做的事情,和 Runnable 一样,不过这个线程是有返回值的。FutureTask 我们叫做任务,入参是 Callable,是对 Callable 的包装,方便线程池的使用。我们来看下 Callable 接口的定义:
@FunctionalInterface public interface Callable<V> { V call() throws Exception; }
Callable 接口的返回值是一个泛型,可以定义成任何类型,但我们使用的时候,都不会直接使用 Callable,而是会结合 FutureTask 一起使用。
2.5.2 谈谈对 FutureTask 的理解
组合了 Callable,实现了 Runnable,把 Callable 和 Runnnable 串联了起来。
- 统一了有参任务和无参任务两种定义方式,方便了使用。
- 实现了 Future 的所有方法,对任务有一定的管理功能,比如说拿到任务执行结果,取消任务,打断任务等等。
2.5.3 谈谈对 FutureTask 的 get、cancel 方法的理解
get 方法主要作用是得到 Callable 异步任务执行的结果,无参 get 会一直等待任务执行完成之后才返回,有参 get 方法可以设定固定的时间,在设定的时间内,如果任务还没有执行成功,直接返回异常,在实际工作中,建议多多使用 get 有参方法,少用 get 无参方法,防止任务执行过慢时,多数线程都在等待,造成线程耗尽的问题。
cancel 方法主要用来取消任务,如果任务还没有执行,是可以取消的,如果任务已经在执行过程中了,你可以选择不取消,或者直接打断执行中的任务。
2.5.4 Runnable 和 Callable 之间如何转化?
Runnable 和 Callable 是通过 FutureTask 进行统一的,FutureTask 有个属性是 Callable,同时也实现了 Runnable 接口,两者的统一转化是在 FutureTask 的构造器里实现的,FutureTask 的最终目标是把 Runnable 和 Callable 都转化成 Callable,Runnable 转化成 Callable 是通过 RunnableAdapter 适配器进行实现的。
线程池的 submit 底层的逻辑只认 FutureTask,不认 Runnable 和 Callable 的差异,所以只要都转化成 FutureTask,底层实现都会是同一套。
2.6 使用 Executor 框架
3. Thread 类的 API(等待/通知机制)
3.1 sleep 方法
sleep 的意思是,当前逻辑执行到此不再继续执行,而是等待指定的时间。但在这段时间内,该线程持有的 Monitor 锁并不会被放弃。我们可以认为线程只是工作到一半休息了一会,但它所占有的资源并不会交还。这样设计很好理解,因为线程在 sleep 的时候可能是处于同步代码块的中间位置,如果此时把锁放弃,就违背了同步的语义。所以 sleep 时并不会放弃锁,等过了 sleep 时长后,可以确保后面的逻辑还在同步执行。
3.1.1 wait() 和 sleep() 的相同点和区别
相同点:两者都让线程进入到 TIMED_WAITING 状态,并且可以设置等待的时间。
不同点:
- wait 释放了锁,它是 Object 类的方法,sleep 是 Thread 类的方法。
sleep 不会释放锁,线程睡眠的时候,其它线程是无法获得锁的,但 wait 会释放锁。
3.2 yield 方法
这个方法用的比较少,yield 单词的意思是让路,在多线程中意味着本线程愿意放弃 CPU 资源,也就是可以让出 CPU 资源。不过这只是给 CPU 一个提示,当 CPU 资源并不紧张时,则会无视 yield 提醒。如果 CPU 没有无视 yield 提醒,那么当前 CPU 会从 RUNNING 变为 RUNNABLE 状态,此时其它等待 CPU 的 RUNNABLE 线程,会去竞争 CPU 资源。讲到这里有个问题,刚刚 yield 的线程同为 RUNNABLE 状态,是否也会参与竞争再次获得 CPU 资源呢?经过我大量测试,刚刚 yield 的线程是不会马上参与竞争获得 CPU 资源的。
3.3 setPriority 方法
此方法用于设置线程的优先级。每个线程都有自己的优先级数值,当 CPU 资源紧张的时候,优先级高的线程获得 CPU 资源的概率会更大。请注意仅仅是概率会更大,并不意味着就一定能够先于优先级低的获取。
3.4 interrupt 方法
interrupt 的意思是打断。调用了 interrupt 方法后,线程会怎么样?不知道你的答案是什么。我在第一次学习 interrupt 的时候,第一感觉是让线程中断。其实,并不是这样。inerrupt 方法的作用是让可中断方法中断,比如让 sleep 中断。也就是说其中断的并不是线程的逻辑,中断的是线程的阻塞。
3.5 join 方法
这个方法功能强大,也很实用。我们用它能够实现并行化处理。比如主线程需要做两件没有相互依赖的事情,那么可以起 A、B 两个线程分别去做。可以在主线程中调用 A、B 的 join 方法,让主线程 block 住,直到 A、B 线程的工作全部完成,才继续走下去。注意这里 block 住的不是A、B线程,而是主线程。
4. Java 正确终止线程的方法
Java 提供了很丰富的 API 但没有为停止线程提供 API。JDK 1.0 本来有一些像 stop(), suspend() 和 resume() 的控制方法,但是由于潜在的死锁威胁。因此在后续的 JDK 版本中被弃用了,之后 Java API 的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当 run() 或者 call() 方法执行完的时候线程会自动结束,如果要手动结束一个线程,可以用 volatile 布尔变量来退出 run() 方法的循环或者是取消任务来中断线程。
4.1 退出标志法
使用退出标志,使线程正常退出。需要 while() 循环在某以特定条件下退出,最直接的办法就是设一个 boolean 标志,并通过设置这个标志来控制循环是否退出:
public class MyThread implements Runnable { private volatile boolean isCancelled; public void run() { while (!isCancelled) { //do something } } public void cancel() { isCancelled=true; } }
注意,isCancelled 需要为 volatile,保证线程读取时 isCancelled 是最新数据。
4.2 interrupt
退出标志法适用于线程正在运行的情况,如果线程是阻塞的,则不能使用退出标志法来终止线程。这时就只能使用 Java 提供的中断机制:
void interrupt()。如果线程处于被阻塞状态(例如处于 sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个 InterruptedException 异常。如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true。被设置中断标志的线程将继续正常运行,不受影响。
- static boolean interrupted()。测试当前线程(正在执行这一命令的线程)是否被中断,这一调用会将当前线程的中断状态重置为 false。
- boolean isInterrupted()。测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态。
在下面的代码中,main
线程通过调用t.interrupt()
方法中断t
线程,但是要注意,interrupt()
方法仅仅向t
线程发出了“中断请求”,至于t
线程是否能立刻响应,要看具体代码。而t
线程的while
循环会检测isInterrupted()
,所以上述代码能正确响应interrupt()
请求,使得自身立刻结束运行run()
方法。
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}