文章结构
一、概述
本篇博客为《Java并发编程的艺术》的学习笔记,主要对于 Thread
的知识做一个记录,知识结构如下:
- 线程的六个状态。
Thread
中start
和run
方法的区别。- 处理线程的返回值之
Callable
。 sleep
和wait
的区别。notify
和notifyAll
的区别。- 如何中断线程。
- 线程的其他方法。
二、线程的六个状态
注:本部分内容摘自《Java并发编程的艺术》。
线程的状态描述如下表所示:
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但还没有调用 start() 方法 |
RUNNABLE | 运行状态,Java 线程将操作系统中的就绪和运行两种状态统称为“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断) |
TIME_WAITING | 超时等待状态,和 WAITING 不同,它是可以在指定的时间内自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
它们之间的关系如下图所示:
- 一个线程被实例化之后,它就进入了初始(NEW)状态。
- 当调用
start()
方法开启一个线程后,该线程就进入了运行中(RUNNABLE)状态。 - 如果线程执行了
wait()
方法,线程就会进入等待(WAITING)状态,进入该状态的线程需要依靠其它线程的通知才能返回运行状态。 - 当线程调用
wait(long)
方法,线程会进入超时等待状态,方法参数设置的就是超时的时间,如果超过了该时间,线程也会返回运行状态。 - 阻塞状态是线程阻塞在进入
synchronized
关键字修饰的方法或代码块(获取锁)时的状态,当线程没能获取锁时就会被阻塞,直到获取锁时才会重新返回运行状态。 - 当线程执行完成之后,即
run()
方法执行完成之后,就会进入到终止(TERMINATED)状态。
三、start 和 run 方法的区别
start
方法和 run
方法的区别在于执行的线程,下面看一个例子:
public class Test {
static class MyThread extends Thread{
MyThread(String name){
super(name);
}
// 打印出当前线程的名字
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
public static void main(String[] args) {
MyThread thread = new MyThread("MyThread#1");
thread.run();
thread.start();
}
}
它的输出结果为:
main
MyThread#1
从结果可以得知调用 run
方法的时候是工作在主线程的,而调用 start
方法的时候才是工作在子线程,所以可以得出如下结论:
- 调用
start()
方法会启动创建好的子线程。 run()
方法只是Thread
的一个普通方法的调用,并不会启动该线程。
三、Callable
Thread
的 run
方法是不提供返回值的,如果需要在线程执行结束之后提供一个返回值,可以考虑实现 Callable
接口,然后通过 FutureTask
或线程池获取。
通过 FutureTask
获取结果的方式如下所示:
public class Worker implements Callable<Boolean> {
@Override
public Boolean call() throws Exception {
Thread.sleep(1000); // 模拟耗时任务
return true;
}
}
首先定义一个 Worker
类,它实现了 Callable
接口,该接口接收一个泛型,该泛型就是返回值的类型。然后在 call()
方法中就可以执行我们的代码逻辑然后返回相应的返回值,它就相当于带有返回值的 run()
方法。
接下来看如何通过 FutureTask
来获取结果:
public class Test {
public static void main(String[] args) {
Worker w = new Worker();
FutureTask<Boolean> task = new FutureTask<>(w);
new Thread(task).start();
try {
getCurrentTime();
boolean success = task.get();
getCurrentTime();
System.out.println("result: "+ success);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void getCurrentTime(){
Date d = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("当前时间:" + sdf.format(d));
}
}
在 main()
方法中,实例化了 Worker
对象,然后传入 FutureTask
的构造方法中构造 FutureTask
对象。接着将 FutureTask
对象传入 Thread
中进行启动,启动步骤就算完成了。
然后在 try/catch
中,通过一个 boolean
值来接收 FutureTask
的返回值,调用的是 FutureTask# get
方法,注意这个方法如果在未获得返回结果的时候会阻塞住所在线程。
所以上述程序运行结果为:
当前时间:2019-03-12 09:49:33
当前时间:2019-03-12 09:49:34
result: true
可以看到,在 1s 之后,我们才得到了返回结果。
而通过线程池获取结果的方式如下所示,我们只需要该写 main
方法:
public static void main(String[] args) {
Worker w = new Worker();
ExecutorService executor = Executors.newCachedThreadPool();
Future<Boolean> future = executor.submit(w);
try {
getCurrentTime();
boolean result = future.get();
getCurrentTime();
System.out.println("result: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
Worker
对象通过 submit
方法提交给线程池进行执行,然后我们通过一个 Future
对象来接收它的返回值,接着在 try/catch
中通过 Future#get
方法来获取返回值,该方法在未获取到返回结果时也会阻塞所在线程,运行结果如下:
当前时间:2019-03-12 09:57:16
当前时间:2019-03-12 09:57:17
result: true
Future
除了 get
方法外,还有其它的方法可以调用,如下所示:
public interface Future<V> {
// 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
boolean cancel(boolean mayInterruptIfRunning);
// 判断Future是否为已关闭状态
boolean isCancelled();
// 判断任务是否完成,需要注意的是如果任务正常终止、异常或取消,都将返回true
boolean isDone();
// 等待任务执行结束,然后获得V类型的结果
V get() throws InterruptedException, ExecutionException;
// 同上面的get功能一样,但有超时时间限制,如果超时则会直接返回
V get(long var1, TimeUnit var3) throws InterruptedException, ExecutionException, TimeoutException;
}
四、sleep 和 wait 的区别
sleep()
方法位于 Thread
类中,它是一个静态方法,我们可以传入参数制定所在线程休眠的时间,例如 Thread.sleep(1000)
就是让所在线程休眠 1s,在休眠的时候,它会让出 CPU 的资源但不会改变锁的行为,此时的线程位于超时等待状态。
wait()
方法位于 Object
类中,它常常和 synchronized
关键字修饰的方法或代码块一起出现,当在同步块中调用 wait()
方法时,此时的线程就会让出其占有的同步锁给其他线程使用,然后进入到同步队列中等待锁。所以 wait()
方法不仅会让出 CPU 资源,它还会改变锁的行为,此时的线程位于等待状态。
它们的区别总结如下:
sleep()
方法位于Thread
类中,wait()
方法位于Object
类中。sleep()
方法和wait()
方法均会让出 CPU 资源,但wait()
方法会改变锁的行为,即释放锁;而sleep()
方法则不会改变锁的行为,它所做的仅仅是让线程“休眠”一段时间。sleep()
方法在到达设置的超时时间之后就会回到运行中状态,而wait()
方法则是让出了同步锁,所以在重新得到同步锁之前它会一直在等待队列中阻塞住。
五、notify 和 notifyAll 的区别
在介绍这两个方法的区别之前,先介绍下两个重要的概念:等待池和锁池。
- 等待池:假设线程 A 调用了某个对象的
wait()
方法,线程 A 就会释放该对象的锁,同时线程 A 就进入到了该对象的等待池中进入到等待池中的线程不会去竞争该对象的锁。 - 锁池:假设线程 A 已经拥有了某个对象的锁,而其他线程 B、C 想要调用这个对象的某个
synchronized
方法(或者块),由于 B、C 线程在进入synchronized
方法(或者块)之前需要先获得该对象锁的所有权,而此时该对象锁正被 A 线程所占用,所以 B、C 线程就会被阻塞,进入到一个地方等待锁的释放,这个地方就是锁池。
接下来需要明晰一个问题:notify
和 notifyAll
方法通知的是谁?
答案是它们通知的都是等待池中处于等待状态的线程。
而它们的区别如下:
notify()
方法会随机通知等待池中的一个线程到锁池中去竞争获得锁的机会。notifyAll()
方法则是通知所有位于等待池的线程进入到锁池中去一起竞争获得锁的机会。
示意图如下所示:
notify:
notifyAll:
总结地说,notify
和 notifyAll
方法所做的事情就只是通知,通知位于等待池中的线程进入到锁池中去竞争锁,而通知的方式是随机通知一个还是通知所有的线程就是它们之间的区别了。
六、如何中断线程
当线程在执行完 run()
方法的逻辑之后,就会进入到终止状态,但对于有些线程来说,它们在正常情况下是永远都不会退出的,例如服务端与客户端保持通信连接的时候,往往是放在一个 while
循环中保持永久的连接,那么在这种情况下如果想要中断线程应当如何选择呢?以下有 3 种方式:
1. stop() 方法
最简单的方式就是直接调用 Thread#stop()
方法就可以直接终止线程的运行了,但是这个方法已经是一个过时的方法了,而且官方也非常不推荐使用该方法来终止线程。因为这种终结线程的方式过于暴力,非常容易导致线程无法正确地释放资源,从而引发程序的不正常状态。
2. 设置标志位停止
我们可以在线程中维护一个 Boolean 类型的成员变量,通过这个成员变量来控制线程的中断退出,示例代码如下所示:
public class MyThread extends Thread {
private boolean exit = false;
@Override
public void run() {
while (!exit){
System.out.println("I'm running...");
}
System.out.println("I'm ready to exit");
}
public void exit(){
this.exit = true;
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(500);
thread.exit();
}
}
运行 main
方法之后,打印信息如下:
I’m running…
I’m running…
…
I’m running…
I’m ready to exit
在打印出一系列的 I'm running
之后,最终跳出了 while
循环,成功终止了该线程。
3. 通过 interrupt 设置
在弃用 stop()
方法之后,官方重新提供了 3 个方法来协助终止线程:
public void interrupt() // 中断线程
public boolean isInterrupted() // 判断线程是否被中断
public static boolean interrupted() // 判断线程是否被中断,并清除当前的中断状态
我们可以将这 3 个方法当作比较温和的中断方式,当我们想要中断线程的时候,我们可以调用 Thread#interrupt()
方法,注意这个方法并不会直接停止线程(要不然不就跟 stop()
一样了嘛),它所做的是将该 Thread
的中断标志位进行置位,线程可以通过 isInterrupted()
方法来检查自身是否已经被设置中断,从而做出相应的逻辑达到停止线程的效果。
而 interrupted()
方法也是用来判断线程是否设置了中断标志位的,但是它还会多做一步工作就是将中断标志位进行复位。
对于会抛出 InterruptedException
的方法,例如 Thread#sleep()
,当它们在执行的过程中线程执行了 interrupt()
方法时,该异常就会被触发,此时我们应当在异常中处理一些相应的逻辑,保证线程的正确终止。
而当线程处于终止状态的时候,无论线程是否被中断过,它的中断标志位都会被复位;而当会抛出 InterruptedException
异常的方法在抛出该异常之前,JVM 会先对中断标志位进行清除,然后再抛出该异常。
示例代码如下:
public class MyThread extends Thread {
@Override
public void run() {
while (!isInterrupted()){ // 如果中断标志位被置位,则会退出循环
// 具体逻辑...
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
thread.interrupt(); // 中断该线程
}
}
而对于在 sleep
过程中发生的中断:
public class MyThread extends Thread {
@Override
public void run() {
while (!isInterrupted()){
try {
sleep(1000);
} catch (InterruptedException e) {
// 具体逻辑...
return; // 通过 return 退出该循环
}
}
System.out.println("I'm out of the while loop");
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
thread.interrupt();
}
}
在异常中我们可以先释放相关资源,然后通过 return
退出该循环。
七、线程的其他方法
在线程中除了常用的 run()
、start()
、interrupt()
等方法之外,还有下面几个方法,介绍如下:
- yield:当调用
Thread#yield
方法时,会给线程调度器一个当前线程愿意让出 CPU 资源的一个暗示,但是线程调度器可能会忽略掉这个暗示。 - join:当调用
Thread#join
方法时,这个方法会挂起调用线程,直到被调用的线程执行结束之后,调用线程才能继续执行。 - suspend:当调用
Thread#suspend
方法时,会将线程挂起,这是一个废弃方法,挂起的时候线程不会释放相关资源(包括锁),所以极易造成死锁状态,不推荐使用。 - resume:将挂起的线程恢复为运行中状态,它是
suspend
的配套方法,所以这也同样是个废弃方法。
希望这篇笔记能对你有所帮助~