Java并发核心知识体系精讲——线程8大核心基础(一)
实现多线程的方法到底是1种还是2种还是4种?
网上的说法

网上说法很多,有两种、三种、四种,每一种说法看起来都很有说服力,都是从不同的角度进行分析。
正确的说法
Oracle官网的文档是如何写的?
翻译成中文后如下:
如图中描述,第一种是声明成Thread类的子类,第二种方法是实现Runnable接口。
oracle官网文档
方法一:实现Runnable接口
1 | public class RunnableStyle implements Runnable{ |
方法二:继承Thread类
1 | public class ThreadStyle extends Thread{ |
两种方法对比
方法一(实现Runnable接口更好)
- 首先从架构角度,线程执行的任务,也就是run中运行的具体代码,应该是和Thread类是解耦的;
- 使用继承Thread类的方法,无法使用线程池;
- 由于java不支持多继承,所以继承了Thread之后就无法再继承其他类,不便于扩展。
两种方法的本质对比
方法一:最总调用target.run()
target就是通过Thread有参构造传入的Runnable接口的实现类,所以再Thread中最终还是会调用Runnable的实现类重写的run()方法
1 |
|
方法二:run()整个都被重写
run()方法整个都被重写之后,Thread最终就会直接调用被重写后run()方法
如果同时使用两种方法会怎么样?
代码如下:
1 | public class BothRunnableThread { |
最终打印结果是我来自Thread,因为在Thread类中,是先调用Thread类的run()方法,然后target.run()再调用作为参数的Runnable接口的实现的run()方法,
而代码中直接重写了Thread的run()方法,就不会再调用target.run()
总结:最精准的描述
- 通常我们可以分为两类,Oracle也是这么说的;
- 准确的讲,创建线程只有一种方法那就是构建Thread类,而实现线程的执行单元有两种方式。
方法一:实现Runnable接口的run方法,并把Runnable实例传给Thread类;
方法二:重写Thread的run方法(继承Thread类)
经典的错误观点
线程池创建线程也算是一种新建线程的方法
对线程池的创建,最终都会调用到如图所示的方法,其中使用了Executors.defaultThreadFactory(),这个工程类就是创建线程池用的
最终在默认情况下,使用的DefaultThreadFactory类,如图中所示,最终还是通过Thread类来创建的线程
通过Callable和FutureTask创建线程,也算是一种新建线程的方式
Callable主要是依赖于FutureTask,而FutureTask是继承于Runnable,最后依然还是需要依靠Thread来进行创建线程
无返回值是实现Runnable接口,有返回值是实现Callable接口,所以Callable是新的实现线程的方式
此观点和上面的观点是一样的,Callable接口获取返回值依靠的是FutureTask,而FutureTask依然是对Runnable的实现
定时器创建线程
代码如下,创建了一个每隔一秒打印线程名称的定时器,定时器的逻辑部分主要是依靠抽象类TimerTask的run()方法来实现的,而TimerTask也是继承与Runnable接口,Timer类内部依然是通过Thread来运行的线程
1 | public class DemoTimerTask { |
匿名内部类创建线程
具体代码如下,直接通过匿名内部类重写Thread或者实现Runnable的run()方法,实际上和通过类继承Thread类或者实现Runnable接口去重写run()没有什么区别
1 | public class AnonymousInnerClassDemo { |
典型错误观点总结
- 多线程的实现方式,在代码中写法千变万化,但其本质万变不离其宗,都是通过之前所说的那两种方法实现的(继承
Thread重写run()方法或实现Runnable接口实现run()方法)
Lambda表达式创建线程
代码如下,实际是通过函数式编程实现的,其本质和实现Runnable的匿名内部类没有区别
1 | public class Lambda { |
如何从宏观和微观两个方面来提升技术?如何了解技术领域的前沿动态?如何在业务开发中成长?
宏观上
- 并不是靠工作年限,有的人工作了5年技术却还只是懂皮毛。
- 要有强大的责任心,不放过任何bug,找到原因并去解决,这就是提高。
- 主动:永远不会觉得自己的时间多余,重构、优化、学习、总结等。
- 敢于承担:虽然这个技术难题以前没有碰到过,但是在一定的了解调研后,敢于承担技术难题,让工作充满挑战,这一次次攻克难关的过程中,进步是飞速的。
- 关系产品,关系业务,而不只是写的代码。
微观上
- 看经典书籍(指外国人写的经典的中国译本,比如说Java并发编程实战、自顶向下计算机网络)
- 看官方文档
- 英文搜google和stackoverflow
- 自己动手,实践写demo,尝试用到项目里面
- 不理解的参考该领域的多个书本,综合判断
- 学习开源项目,分析源码(学习synchronized原理,反编译看cpp代码)
常见面试问题
有多少中实现线程大的方法?
- 从不同的角度看,会有不同的答案
- 典型答案是两种
- 我们看原理,两种本质都是一样的
- 具体展开说其他的方式
- 结论
实现Runnable接口和继承Thread类那种方式更好
- 从代码架构角度(可以将线程分为两个部分,一是线程运行的具体任务,也就是实现run方法中的内容,二是线程的生命周期,也就是创建线程启动线程运行线程结束线程,这部分是
Thread类实现的) - 新建线程大的损耗(继承
Thread类的话,每次我们想要运行任务都需要创建一个线程,而实现Runnable接口的话,可以使用线程池,每次都使用同一个线程运行)【继承Thread类应该也是可以使用线程池的,只要不调用start方法去创建线程,线程池需要的是一个Runnable的实现类,而Thread本身也是Runnable接口大的实现类】 - Java不支持双继承
怎样才是正确的线程启动方式?
start()和run()的比较
代码如下:
1 | public class StartAndRunMethhodd { |
结果:
1 | main |
start()方法原理解读
start()方法的含义
启动新线程
当主线程在调用了
start方法之后,会告诉jvm需要启动一个新大的线程,至于线程何时能够启动,是由线程调度器决定的,
并不是说调用了start方法之后线程就已经开始运行了,调用start方法只是一个通知,有可能立刻就还会执行,也可能会等一会执行,
调用start方法之后,是有两个线程在执行的,主线程以及开启的新线程准备工作
新线程在执行之前是需要做一些准备工作,首先会进入到就绪状态,此时已经获取到了除了cpu之外的其他资源,比如上下文内容等,
之后会被调度到执行状态,在执行状态才能去等待cpu资源获取cpu资源不能重复调用
start()会抛出异常,如下图,一旦线程开始运行,就会更改状态,运行完毕就会更改为终止状态,无法在切换到其他状态,所以第二次执行就会抛出异常

start()源码解析
启动新线程检查线程状态
如下图,可以看到start方法中,第一步就是先去检查线程的状态是不是0
线程状态threadStatus字段初始值为0
加入线程组
如上面的图所示,可以看到,检查完线程状态,会加入有个group组group.add(this);
start0()
之后就会调用start0()方法,start0()方法是一个native方法,是由C++实现的
run()方法原理解读
直接调用run方法,其实就是调用一个普通的方法,并不会启动新的线程,只是一个对象调用了一个普通的方法。
常见面试题
一个线程两次调用start()方法,会出现什么情况?为什么?
刚刚上面已经讲解
既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?
上面已经讲解
上山容易下山难——如何正确停止线程?
讲解原理
- 原理介绍:使用
interrupt来通知,而不是强制interrupt是通过启用一个线程来通知另一个线程停止的操作,java中没有强制停止一个线程的机制,只能做到通知,
被停止的线程拥有最高权限,它可以自己判断是否要停止,
因为被停止的线程一般收到停止通知之后,会需要做一些收尾工作,比如数据回滚数据保存之类,所以通知线程只能是通知,无法做到强制。
最佳实践:如何正确停止线程
通常线程会在什么情况下停止
run方法的所有代码都运行完毕,线程会自动停止- 有异常出现,并且方法中没有捕获异常会导致线程停止
正确的停止的方法:interrupt
通常线程会在什么情况下停止普通情况
代码如下所示,Thread.currentThread().isInterrupted()方法是查看线程是否中断,需要不停大的在循环中查看线程状态,才能正确的停止线程,
否则的话线程是不会理会停止操作的。
1 | public class RightWayStopThreadWithoutSleep implements Runnable { |
线程可能被堵塞
代码如下所示,我们要做的就是让线程处于sleep的时候收到中断通知
1 | public class RightWayStopThreadWithSleep { |
结果如图所示,当线程处于sleep的时候收到中断通知,会直接抛出异常
如果线程每次迭代后都堵塞
代码如下所示,在这种情况下,我们就没必须每次循环都调用Thread.currentThread().isInterrupted()去检查线程是否已经中断,因为每次循环的时候都会进行sleep,sleep对中断做了处理
1 | public class RightWayStopThreadWithSleepEveryLoop { |
while内try/catch的问题
- 如果将
try/catch放到循环内部的话,中断会失败,因为上面的代码其实是依靠异常来中断了循环,然后再被循环外面的try/catch处理,而如果把try/catch放到循环里面,就无法依靠异常中断循环,所以循环将会继续 - 把
try/catch循环里面在while循环条件里面添加上Thread.currentThread().isInterrupted()判断是否中断,这样也是无效的,因为线程在获取到一次标记之后,就会清除标记,所以如果sleep获取了标记,Thread.currentThread().isInterrupted()就无法再成功检查了(极小概率再sleep之后,while循环之前收到中断通知,这样再进行判断的话就会中断循环,但概率极小)
实际开发中的两种最佳实践
优先选择:传递中断
如代码中所示,如果我们在调用其他方法的时候,有sleep的操作,那么不应该在方法里面捕获异常,而应该将异常上抛得到run方法里面,否则run方法里面无法感知到线程中断通知
1 | public class RightWayStopThreadInProd implements Runnable{ |
不想或无法传递:恢复中断
如下代码所示,如果下级方法想要自己处理异常,那么可以调用Thread.currentThread().interrupt()将中断通知恢复,以便于上级run方法感应到中断通知
1 | public class RightWayStopThreadInProd2 implements Runnable { |
不应该屏蔽中断
既不在方法签名中抛出,也不在捕获异常后重新恢复中断,这样会导致上级run方法无法收到中断通知,以至于无法终止操作
响应中断的方法总结列表
- Object.wait() / wait(long) / wait(long, int)
- Thread.sleep(long) / sleep(long, int)
- Thread.join() / join(long) / join(long, int)
- java.util.concurrent.BlockingQueue.task() / put(E)
- java.util.concurrent.locks.Lock.lockInterruptibly()
- java.util.concurrent.CountDownLatch.await()
- java.util.concurrent.CyclicBarrier.await()
- java.util.concurrent.Exchanger.exchange(V)
- java.nio.channels.InterruptibleChannel 相关方法
- java.nio.channels.Selector相关方法
正确停止带来的好处
使用interrupt来停止线程,可以由被停止的线程来判断要不要停止,也可以让被停止大的线程做好一些收尾工作
停止线程的错误方法
被弃用的stop,suspend和resume方法
- stop方法会导致线程突然停止,这样对线程中运行的代码是非常不友好的,假如正在处理一个任务,想要撤销,有时候会需要对一些数据进行回滚,突然停止会导致数据无法进行处理
suspend和resume方法,挂起和唤醒,suspend方法在挂起的时候并不会释放它已经持有大的锁,而如果需要唤醒它的线程需要获取锁才能唤醒,就会导致死锁
用volatile设置boolean标记位
看上去可行(有些时候确实是可用的)
代码如下,通过更改canceled字段的值,来使线程停止
1 | public class WrongWayVolatile implements Runnable{ |
错误之处(有些时候会报错)
代码如下,生产者以较快的速度生产任务,消费者以较慢的速度消费任务,当队列满了之后,就会将生产者的异步堵塞,而这个时候,即便是更改canceled字段的值,代码也无法检测到
1 | public class WrongWayVolatileCantStop { |
修正方案
修正后的代码如下,使用interrupt()方法中断线程,并在循环的时候检查线程状态
1 | public class WrongWayVolatileFixed { |
重要函数的源码解析
interrupt()方法
如图所示,interrupt()方法中的主要逻辑都是调用interrupt0()方法实现的,interrupt0()方法是一个native方法

判断是否已被中断的相关方法
static boolean interrupted()
- 返回当前线程是否被中断,获取之后清除当前状态
- 这个静态方法的目标对象是调用它的线程
- 如下图,参数为true,表示要清除状态

boolean isInterrupt()
- 返回当前线程是否被中断,获取之后不清除状态
- 如下图,参数为false,表示不进行清除

Thread.interrupted()的目标对象
- 目标对象是调用它的线程,即便使用的是其他线程的对象调用的这个静态方法,得到的结果依然是当前线程的
- 如果再main主线程中调用,无论是使用Thread直接调用,还是通过开启的多线程对象调用,得到的结果都是主线程的结果。
常见面试问题
如何停止线程
- 原理:用
interrupt来请求、好处(使用interrupt停止线程,只是通知线程需要中断,可以给线程预留处理后续数据的时间) - 想要停止线程,需要请求方、被停止方、子方法被调用方相互配合(请求方发出中断通知,被停止方需要在循环中监听中断状态,子方法需要将中断异常上抛,或者捕获后重新恢复中断状态)
- 最后再说错误的方法:
stop/suspend已废弃,volatile的boolean无法处理长时间阻塞的情况(stop方法会直接停止线程,导致线程无法正确回滚数据;suspend方法可能会导致死锁;volatile+boolean的方法,在阻塞的情况下是无法中断线程的)
如何处理不可中断阻塞
- 没有通用的处理方法,只能根据不同的场景,尽量使用能够响应线程中断的方法
线程的一生——6个状态(生命周期)
有哪6中状态?每个状态是什么含义?
New
已创建还未启动
当我们使用new Thread()创建了一个线程,还没有执行start方法,这个时候这个线程就处于这个状态Runnable
可运行/运行中
一旦线程调用了start方法,就会处于这个状态,有可能这个时候线程调度器还没有分配线程进行执行(此时可以称为可运行状态)
cpu分配了资源,进入运行状态,此时为运行中,依然是Runnable状态(运行中)
如果cpu调度器将资源突然调度到其他地方,那么该线程依然为Runnable状态(可运行)Blocked
被阻塞
进入synchronized代码块并且没有拿到锁时,处于该状态
当线程拿到锁之后,会切换到Runnable状态执行
只有synchronized锁,会进入此种状态Waiting
不计时等待
当调用这几个方法的时候,会使线程进入当前状态:Object.wait() \ Thread.join() \ LockSupport.park()
当调用这几个方法时,会唤醒等待,使线程重新进入Runnable状态:Object.notify() \ Object.notifyAll() \ LockSupport.unpark()
注意:方法都是不带时间的,如果代理时间参数,就会进入到计时等待Timed Waiting
计时等待
调用这几个方法,会进入当前状态:Thread.sleep(time) \ Object.wait(time) \ Thread.join(time) \ LockSupport.parkNanos(time) \ LockSupport.parkUntil(time)
当达到等待时间,或者调用这几个方法,会唤醒线程进入Runnable状态:Object.notify() \ Object.notifyAll() \ LockSupport.unpark()Terminated
已终止
当run方法已经执行完毕,会进入当前状态
或者抛出一个未捕获的异常,也会进入当前状态
状态间的转化图示
状态之间转换代码演示
- NEW -> RUNNABLE -> RUNNABLE -> TERMINATED
代码未执行start方法时状态为 NEW
代码执行start方法后状态为 RUNNABLE (此时无法分辨是还未调用cpu资源还是已经调用cpu资源)
run在循环中,状态为 RUNNABLE
run方法运行结束后,状态为 TERMINATED
1 | public class NewRunnableTerminated implements Runnable{ |
- TIMED_WAITING -> BLOCKED -> WAITING
线程1进入了
syn()方法,执行了休眠1秒,此时的状态为 TIMED_WAITING
因为syn()方法是synchronized修饰的,线程1进入之后,线程2无法再拿到锁,所以此时线程2的状态为 BLOCKED
线程1休眠1秒种之后,进入wait()方法,此时状态为 WAITING1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class BlockedWaitingTimedWaiting implements Runnable {
public static void main(String[] args) throws InterruptedException {
BlockedWaitingTimedWaiting runnable = new BlockedWaitingTimedWaiting();
Thread thread1 = new Thread(runnable);
thread1.start();
Thread thread2 = new Thread(runnable);
thread2.start();
Thread.sleep(10);
System.out.println(thread1.getState());
System.out.println(thread2.getState());
Thread.sleep(1200);
System.out.println(thread1.getState());
}
public void run() {
syn();
}
private synchronized void syn() {
try {
Thread.sleep(1000);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
阻塞状态是什么?
- 一般习惯而言,把
Blocked(被阻塞)、Waiting(等待)、Timed_Waiting(计时等待)都称为阻塞状态 - 不仅仅是
Blocked
常见面试问题
线程有哪几种状态?生命周期是什么?
具体回答可以根据上面的线程状态流转图进行回答
Thread和Object类中的重要方法详解
方法概览
| 类 | 方法名 | 简介 |
|---|---|---|
| Thread | sleep相关 | 本表格的”相关”,指的是重载方法,也就是方法名相同,但参数个数不同,例如sleep有多个方法,只是参数不同,实际作用大同小异 |
| join | 等待其他线程执行完毕 | |
| yield相关 | 放弃已经获取到的cpu资源 | |
| currentThread | 获取当前执行线程的引用 | |
| start,ren相关 | 启动线程相关 | |
| interrupt相关 | 中断线程 | |
| stop(),suspend(),resuem()相关 | 已废弃 | |
| Object | wait/notify/notifyAll相关 | 让线程暂停时休息和唤醒 |
wait,notify,notifyAll方法详解
作用,用法
阻塞阶段
- 执行
wait方法进入阻塞阶段的时候,必须要拥有monitor锁 - 进入阻塞阶段之后,直到以下4种情况之一发生时,才会被唤醒
1)另一个线程调用这个对象的
notify()方法且刚好被唤醒的是本线程
2)另一个线程调用这个对象的notifyAll()方法
3)过了wait(long timeout)规定的超时时间,如果传入0就是永远等待
4)线程自身调用了interrupt()
唤醒阶段
notify(),随机唤醒一个线程notifyAll(),唤醒所有的线程
遇到中断
- 会抛出
InterruptedException异常,然后释放锁
代码演示:4种情况
普通用法
代码如下所示
我们先启动线程1,使线程1先进入到synchronized代码块中,并且已经执行了wait方法,然后在启动线程2
由打印结果我们可以看出来,线程1在执行了wait方法之后,是已经释放锁了的,不然线程2无法进入同步代码块执行notify方法,也无法将线程1唤醒
由结果打印顺序也可以看出,我们是先运行了线程1,打印线程已经执行,然后线程2调用notify方法,之后打印输出,然后线程1再打印输出
所以,线程2将线程1唤醒之后,线程1是处于阻塞状态,不会立即执行代码,而是会等线程2执行完毕,线程1才能再次获取锁,然后开始执行
1 | public class Wait { |
打印结果
1 | Thread-0已经开始执行了 |
notify和notifyAll展示
代码如下所示
线程A和线程B同时启动,然后串行执行同步代码块,线程A执行到wait方法,释放锁,线程B开始执行,线程B执行到wait方法后,释放锁
等待200毫秒之后,线程C开始执行,线程C执行了notifyAll方法,唤醒了阻塞中的线程A和线程B,然后线程A和线程B执行结束
如果使用的是notify方法的话,那么线程A和线程B只能唤醒一个,另一个将进行无尽的等待
1 | public class WaitNotifyAll implements Runnable { |
运行结果:
1 | Thread-0获取到锁 |
只释放当前monitor展示
创建两把锁,A和B
线程1先获取锁A,然后获取锁B,然后释放锁A
线程2先获取锁A,然后尝试获取锁B
从结果可以看出,线程2是无法获取锁B的,因为线程1只释放了锁A
只释放当前monitor,不会影响已经拿到的其他monitor
1 | public class WaitNotifyReleaseOwnMonitor { |
运行结果:
1 | 线程1获取到resourceA锁 |
特点,性质
- 调用wait或者notify或者notifyAll必须先拥有monitor(否则还会抛异常:IllegalMonitorStateException)
- notify只能唤醒一个,这一个是随机唤醒,无法指定
- 这三个方法都属于Object类,所以所有对象都可以调用这三个方法
- 类似功能的Condition(依赖与Lock锁)
- 同时持有多把锁大的情况(释放锁的时候要注意是多把锁,需要避免死锁)
手写生产者消费者设计模式
代码如下所示,使用
wait和notify实现生产者消费者
1 | public class ProducerConsumerModel { |
常见面试题
用程序实现两个线程交替打印0~100的奇偶数
基本思路:
synchronized使用synchronized实现代码如下
使用位移运算符判断奇偶数进行打印,打印后自增1
两个线程使用同一把锁,如果线程抢到了锁,判断不符合自己的条件,就只是空循环,打印也不会加1
这个方法的问题就是可能会等导致线程多次抢到锁之后空转1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class WaitNotifyPrintOddEvenSyn {
private static int count;
private static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 0) {
System.out.println(Thread.currentThread().getName() + ": " + count++);
}
}
}
}
}, "偶数").start();
new Thread(new Runnable() {
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 1) {
System.out.println(Thread.currentThread().getName() + ": " + count++);
}
}
}
}
}, "奇数").start();
}
}更好的办法:
wait/notify使用wait/notify的代码如下
线程打印完之后,会去唤醒另外一个线程,然后自己进行休眠,线程交替唤醒交替休眠,第一个启动的线程为偶数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class WaitNotifyPrintOddEvenWait {
static int count ;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new TurningRunner(),"偶数").start();
new Thread(new TurningRunner(),"奇数").start();
}
static class TurningRunner implements Runnable{
public void run() {
while (count <= 100){
synchronized (lock){
System.out.println(Thread.currentThread().getName()+":"+count++);
lock.notify();
if (count <= 100){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
手写生产者消费者设计模式
- 上面已经有了具体实现方法
为什么wait需要再同步代码块中使用,而sleep不需要
java语言设计的时候为了防止死锁
假设一个线程在执行代码,后面是调用wait方法,另一个线程后面是notify方法,如果不能同步的话,可能会存在执行到wait之前,先执行了notify方法,这样就会导致第一个线程在调用了wait之后陷入永久等待
而sleep所影响的只是当前线程所以可以不在同步代码块中
为什么线程通信的方法wait()、notify()、notifyAll()被定义在Object类里面?而sleep定义在Thread类里?
因为
wait()、notify()、notifyAll()方法是属于锁级别的,而每一个锁都是一个对象
java实现锁的原理也是在对象头中做标记
如果将这三个方法放到每个线程中,每个线程也可能会持有多把锁,这样每次休眠和唤醒会非常麻烦
wait方法是属于Object对象的,那调用Thread.wait会怎么样?
Thread也可以做锁对象,但是Thread比较特殊,最好不要用它来做锁对象
在线程退出的时候,会调用Thread类的notifyAll方法,所以如果使用Thread类做锁对象可能会导致线程总是被无故唤醒
如何选择用notify还是notifyAll
主要看是像唤醒多个线程还是一个线程,根据具体情况进行选择
notifyAll之后所有的线程都会再次抢夺锁,如果某个线程抢夺失败怎么办?
其他被唤醒但是没有获得锁的线程,会陷入阻塞状态,继续等待拿到锁
用suspend()和resume()来阻塞线程可以吗?为什么?
上面已经做出了解释
sleep方法详解
- 作用:我只想让线程在预期的时间执行,其他时候不要占用CPU资源
sleep方法不释放锁
- 包括
synchronized和lock - 和
wait不同
sleep方法响应中断
- 抛出
InterruptedException异常 - 清理中断状态
代码如下,在sleep中被中断,还会抛出异常,然后继续执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class SleepInterrupted implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SleepInterrupted());
thread.start();
Thread.sleep(6500);
thread.interrupt();
}
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("我被中断了");
e.printStackTrace();
}
}
}
}
总结
- sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常并清除中断状态
常见面试题
wait/notify、sleep异同(方法属于那个对象?线程状态怎么切换?)
相同
阻塞
响应中断不同
同步方法
释放锁
指定时间
所属类
join方法
- 作用:因为新的线程加入了我们,所以我们要等待他执行完再出发
- 用法:main等待thread1执行完毕,注意谁等谁
普通用法
执行join之后,主线程会进行等待,等子线程执行完毕之后才会继续运行后面的代码
1 | public class Join { |
遇到中断
join抓取的中断异常是主线程的中断异常
1 | public class JoinInterrupted { |
在join期间,线程是什么状态?:waiting
join期间,主线程处于
Waiting状态
1 | public class JoinThreadState { |
- CountDownLatch或CyclicBarrier是经过封装的,可以实现和join一样的功能
join源码
由图可以看出,join方法也是通过wait实现的
而在Thread线程结束后,会调用一次notifyAll方法,唤醒线程,具体代码在C++部分
这也不能用Thread类做锁的原因

线程退出源码

join等价代码
可以使用wait来进行替换,上面的源码已经显示,线程在退出的时候,会唤醒所有的线程,所以在线程运行完毕之后,会唤醒wait的休眠
1 | public class JoinPrinciple { |
面试常见问题
在join期间,线程处于哪种线程状态?
- 处于Waiting状态
yield方法
- 作用:释放我的CPU时间片(释放时间片之后,状态依然是Runnable状态,随时有可能会被CPU调度执行)
- 定位:JVM不保证遵循(根据具体的情况,有可能不生效,不同的cpu实现原理也可能会导致有不同的效果)
- yield和sleep的区别:是否随时可能再次被调度(sleep线程认为是阻塞的,不会再调度起来,yield则随时可能被调度起来)
获取当前执行线程的引用:Thread.currentThread()方法
- 返回当前所执行的线程的引用
start和run方法
- 前面已经有介绍
- start会启动一个线程,run则只是调用对象的普通方法
stop,suspend,resume方法
- 已弃用
线程的各个属性
| 属性名称 | 用途 | 注意事项 |
|---|---|---|
| 编号(ID) | 每个线程都有自己的ID,用于标识不同的线程 | 被后续创建大的线程使用;唯一性;不允许被修改 |
| 名称(Name) | 作用让用户或者程序员在开发、调试或运行过程中,更容易区分不同的线程、定位问题等。 | 清晰有意义的名字;默认的名字 |
| 是否是守护线程(isDaemon) | true代表该线程是【守护线程】,false代表线程是非守护线程,也就是【用户线程】 | 二选一;继承父线程;setDaemon |
| 优先级(Priority) | 优先级这个属性的目的是告诉线程调度器,用户希望哪些线程相对多运行、那些少运行 | 默认和父线程的优先级相等;共10个等级;默认值是5;不应该依赖 |
线程Id
- 每个线程都有一个Id
- 线程Id是不能修改的
- 线程的Id是自增的,从1开始,main线程是第一个Id
- 但是在main方法里面创建的第一个线程绝对不是2,因为jvm启动大的时候会创建一些其他的线程
- Thread类中获取Id的方法入下,
threadSeqNumber初始为0,因为++在前,所以第一个线程的Id是1

线程名字
- 线程的名称如果没有设置默认是’Thread_0’,后面的数字递增
- 创建线程如果没有设置名称,会如下图设置一个默认名称,名称后面的编号是递增,从0开始


- 线程名称如下图,会设置两个名称,一个线程在java层面的名称,一个是在native的名称
- 设置名称时会判断线程状态,如果线程已经不是未启动状态,无法修改native层面的名称

守护线程
守护线程和普通线程的区别
- 整体无区别(都是为了执行任务,不过有的是用户的任务,有的是JVM的任务)
- 唯一的区别在于JVM的离开(用户线程会影响JVM是否退出,守护线程不会影响)
常见面试问题
守护线程和普通线程的区别
答案同上
我们是否需要给线程设置为守护线程
不应该设置为守护线程,因为如果现在在处理数据,设置成守护线程,JVM退出的时候不会考虑它是否还在运行
线程优先级
- 优先级一共由10个级别,线程的优先级默认是5
- 程序设计不应该依赖于优先级
不同的操作系统对优先级的理解是不一样的(每种操作系统的优先级级别设置是不一样的,需要一些映射,这样可能会和原本的期望不一致)
优先级会被操作系统修改()
未捕获异常如何处理?
线程未捕获异常UncaughtException应该如何处理?
- 可以使用
UncaughtExceptionHandler类来进行处理
为什么要用UncaughtExceptionHandler处理?
- 主线程可以轻松发现异常,子线程却不行
- 子线程的异常无法使用传统办法捕获
- 如果不对子线程的异常进行处理,那么会导致线程中断,任务无法处理,不利于程序的健壮性
两种解决办法
- 方案一(不推荐):手动在每个run方法里面进行try/catch
- 方案二(推荐):利用
UncaughtExceptionHandlerUncaughtExceptionHandler接口

异常处理器的调用策略
- 默认线程处理器(ThreadGroup)逻辑如下代码实现异常捕获器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public void uncaughtException(Thread t, Throwable e) {
//默认情况下parent是null
if (parent != null) {
parent.uncaughtException(t, e);
} else {
//调用Thread.setDefaultUncaughtExceptionHandler(...)
//方法设置的全局handler进行处理
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
//全局handler也不存在就输出异常栈
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
新建捕获器:具体使用:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private String name;
public MyUncaughtExceptionHandler(String name) {
this.name = name;
}
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.WARNING, "线程异常,终止了" + t.getName(), e);
System.out.println(name + "捕获了异常" + t.getName() + "异常:" + e);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class UseOwnUncaughtExceptionHandler implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));
new Thread(new CantCatchDirectly(),"MyThread-1").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(),"MyThread-2").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(),"MyThread-3").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(),"MyThread-4").start();
}
public void run() {
throw new RuntimeException();
}
}常见面试问题
Java异常体系
如何全局处理异常?为什么要全局处理异常?不处理行不行?
上面已经有了讲解run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
- run方法不能再向上抛出异常
- 如果是可检查异常,只能再run方法中进行捕获处理
- 如果是不可检查异常,抛出后,线程会终止运行
线程中如何处理某个未处理异常
- UncaughtExceptionHandler
双刃剑:多线程会导致的问题
线程安全
什么是线程安全
不管业务中遇到怎样的多个线程访问某个对象或者方法的情况,而再编译这个业务逻辑的时候,都不需要额外等地的做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错,就可以称为线程安全)
什么情况下会出现线程安全问题,怎么避免?
运行结果错误:a++多线程下出现消失的请求现象
最终代码演示如下所示
1 | public class MultiThreadError implements Runnable { |
活跃性问题:死锁、活锁、饥饿
死锁示例代码如下
1 | public class MultiThreadError implements Runnable{ |
对象发布和初始化的时候的安全问题
什么是发布
- 对象可以在超过当前类的范围外使用
- public修饰的属性对象
- 一个方法return出一个对象
- 将一个对象作为参数传到其他类的方法中
什么是逸出
- 1.方法返回一个private对象(private的本意是不让外部访问)
- 2.还未完成初始化(构造函数没有完全执行完毕)就把对象提供给外界,比如:
- 在构造函数中未初始化完毕就this赋值
- 隐式逸出——注册监听事件
- 构造函数中运行线程
各种需要考虑线程安全的情况
- 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
- 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check0then-act
- 不同的数据之间存在捆绑关系的时候
- 我们使用其他类的时候,如果对方没有声明自己是线程安全的
为什么多线程会带来性能问题
调度:上下文切换
- 主要是值当线程发生调度的时候
- 当可运行的线程数超过了CPU的核数,就会开始调度线程,以便于让每个线程都有机会运行
什么是上下文
- 主要和一些程序计数器相关
- 切换的时候首先需要挂起一个线程,将线程的运行状态保存得到内存中的某处
- 包括线程运行时需要的一些数据
缓存开销
- CPU为了运行速度,会先在通过计算,在内存中缓存一些可能用到的数据
- 而进行了上下文切换之后,这些缓存的数据大部分都已经没用了
何时会导致密集的上下文切换
- 频繁的抢锁,频繁的IO操作
协作:内存同步
- 线程在运行的时候,会在线程内部缓存一些数据,如果使用锁,会强制同步线程的缓存数据和主内存的数据
- 使线程缓存数据失效,只能使用主内存数据