Java并发核心知识体系精讲——线程8大核心基础(一)

实现多线程的方法到底是1种还是2种还是4种?

网上的说法

多线程实现方式网上说法
网上说法很多,有两种、三种、四种,每一种说法看起来都很有说服力,都是从不同的角度进行分析。

正确的说法

Oracle官网的文档是如何写的?
Orceal官网多线程实现方法-英文
翻译成中文后如下:
Orceal官网多线程实现方法-中文
如图中描述,第一种是声明成Thread类的子类,第二种方法是实现Runnable接口。
oracle官网文档

方法一:实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
public class RunnableStyle implements Runnable{

public static void main(String[] args) {
new Thread(new RunnableStyle()).start();
}

@Override
public void run() {
System.out.println("用Runnable方法实现线程");
}
}

方法二:继承Thread类

1
2
3
4
5
6
7
8
9
10
11
public class ThreadStyle extends Thread{

public static void main(String[] args) {
new ThreadStyle().start();
}

@Override
public void run() {
System.out.println("用Thread方法实现线程");
}
}

两种方法对比

方法一(实现Runnable接口更好)

  • 首先从架构角度,线程执行的任务,也就是run中运行的具体代码,应该是和Thread类是解耦的;
  • 使用继承Thread类的方法,无法使用线程池;
  • 由于java不支持多继承,所以继承了Thread之后就无法再继承其他类,不便于扩展。

两种方法的本质对比

方法一:最总调用target.run()

target就是通过Thread有参构造传入的Runnable接口的实现类,所以再Thread中最终还是会调用Runnable的实现类重写的run()方法

1
2
3
4
5
6
@Override
public void run() {
if (target != null) {
target.run();
}
}
方法二:run()整个都被重写

run()方法整个都被重写之后,Thread最终就会直接调用被重写后run()方法

如果同时使用两种方法会怎么样?

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BothRunnableThread {

public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我来自Runnable");
}
}){
@Override
public void run() {
System.out.println("我来自Thread");
}
}.start();
}
}

最终打印结果是我来自Thread,因为在Thread类中,是先调用Thread类的run()方法,然后target.run()再调用作为参数的Runnable接口的实现的run()方法,
而代码中直接重写了Threadrun()方法,就不会再调用target.run()

总结:最精准的描述

  • 通常我们可以分为两类,Oracle也是这么说的;
  • 准确的讲,创建线程只有一种方法那就是构建Thread类,而实现线程的执行单元有两种方式。

    方法一:实现Runnable接口的run方法,并把Runnable实例传给Thread类;
    方法二:重写Thread的run方法(继承Thread类)

经典的错误观点

线程池创建线程也算是一种新建线程的方法

对线程池的创建,最终都会调用到如图所示的方法,其中使用了Executors.defaultThreadFactory(),这个工程类就是创建线程池用的
线程池创建图

最终在默认情况下,使用的DefaultThreadFactory类,如图中所示,最终还是通过Thread类来创建的线程
DefaultThreadFactory类图

通过CallableFutureTask创建线程,也算是一种新建线程的方式

Callable主要是依赖于FutureTask,而FutureTask是继承于Runnable,最后依然还是需要依靠Thread来进行创建线程

无返回值是实现Runnable接口,有返回值是实现Callable接口,所以Callable是新的实现线程的方式

此观点和上面的观点是一样的,Callable接口获取返回值依靠的是FutureTask,而FutureTask依然是对Runnable的实现

定时器创建线程

代码如下,创建了一个每隔一秒打印线程名称的定时器,定时器的逻辑部分主要是依靠抽象类TimerTask的run()方法来实现的,而TimerTask也是继承与Runnable接口,Timer类内部依然是通过Thread来运行的线程

1
2
3
4
5
6
7
8
9
10
11
12
public class DemoTimerTask {

public static void main(String[] args) {
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},1000,1000);
}
}

匿名内部类创建线程

具体代码如下,直接通过匿名内部类重写Thread或者实现Runnablerun()方法,实际上和通过类继承Thread类或者实现Runnable接口去重写run()没有什么区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AnonymousInnerClassDemo {
public static void main(String[] args) {
new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
}
}

典型错误观点总结

  • 多线程的实现方式,在代码中写法千变万化,但其本质万变不离其宗,都是通过之前所说的那两种方法实现的(继承Thread重写run()方法或实现Runnable接口实现run()方法)

Lambda表达式创建线程

代码如下,实际是通过函数式编程实现的,其本质和实现Runnable的匿名内部类没有区别

1
2
3
4
5
public class Lambda {
public static void main(String[] args) {
new Thread(()-> System.out.println(Thread.currentThread().getName())).start();
}
}

如何从宏观和微观两个方面来提升技术?如何了解技术领域的前沿动态?如何在业务开发中成长?

宏观上

  • 并不是靠工作年限,有的人工作了5年技术却还只是懂皮毛。
  • 要有强大的责任心,不放过任何bug,找到原因并去解决,这就是提高。
  • 主动:永远不会觉得自己的时间多余,重构、优化、学习、总结等。
  • 敢于承担:虽然这个技术难题以前没有碰到过,但是在一定的了解调研后,敢于承担技术难题,让工作充满挑战,这一次次攻克难关的过程中,进步是飞速的。
  • 关系产品,关系业务,而不只是写的代码。

微观上

  • 看经典书籍(指外国人写的经典的中国译本,比如说Java并发编程实战、自顶向下计算机网络)
  • 看官方文档
  • 英文搜google和stackoverflow
  • 自己动手,实践写demo,尝试用到项目里面
  • 不理解的参考该领域的多个书本,综合判断
  • 学习开源项目,分析源码(学习synchronized原理,反编译看cpp代码)

常见面试问题

有多少中实现线程大的方法?

  • 从不同的角度看,会有不同的答案
  • 典型答案是两种
  • 我们看原理,两种本质都是一样的
  • 具体展开说其他的方式
  • 结论

实现Runnable接口和继承Thread类那种方式更好

  • 从代码架构角度(可以将线程分为两个部分,一是线程运行的具体任务,也就是实现run方法中的内容,二是线程的生命周期,也就是创建线程启动线程运行线程结束线程,这部分是Thread类实现的)
  • 新建线程大的损耗(继承Thread类的话,每次我们想要运行任务都需要创建一个线程,而实现Runnable接口的话,可以使用线程池,每次都使用同一个线程运行)【继承Thread类应该也是可以使用线程池的,只要不调用start方法去创建线程,线程池需要的是一个Runnable的实现类,而Thread本身也是Runnable接口大的实现类】
  • Java不支持双继承

怎样才是正确的线程启动方式?

start()run()的比较

代码如下:

1
2
3
4
5
6
7
8
9
10
public class StartAndRunMethhodd {
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName());
};
runnable.run();
new Thread(runnable).start();
}
}

结果:

1
2
main
Thread-0

start()方法原理解读

start()方法的含义

  • 启动新线程

    当主线程在调用了start方法之后,会告诉jvm需要启动一个新大的线程,至于线程何时能够启动,是由线程调度器决定的,
    并不是说调用了start方法之后线程就已经开始运行了,调用start方法只是一个通知,有可能立刻就还会执行,也可能会等一会执行,
    调用start方法之后,是有两个线程在执行的,主线程以及开启的新线程

  • 准备工作

    新线程在执行之前是需要做一些准备工作,首先会进入到就绪状态,此时已经获取到了除了cpu之外的其他资源,比如上下文内容等,
    之后会被调度到执行状态,在执行状态才能去等待cpu资源获取cpu资源

  • 不能重复调用start()

    会抛出异常,如下图,一旦线程开始运行,就会更改状态,运行完毕就会更改为终止状态,无法在切换到其他状态,所以第二次执行就会抛出异常

多次执行start方法抛出异常

start()源码解析

启动新线程检查线程状态

如下图,可以看到start方法中,第一步就是先去检查线程的状态是不是0
Thread中start方法
线程状态threadStatus字段初始值为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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RightWayStopThreadWithoutSleep implements Runnable {

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadWithoutSleep());
thread.start();
Thread.sleep(2000);
thread.interrupt();
}

@Override
public void run() {
int num = 0;
while (!Thread.currentThread().isInterrupted() && num <= Integer.MAX_VALUE/2){
if (num % 10000 == 0){
System.out.println(num+"是10000的倍数");
}
num++;
}
System.out.println("任务运行结束了");
}
}

线程可能被堵塞

代码如下所示,我们要做的就是让线程处于sleep的时候收到中断通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RightWayStopThreadWithSleep {

public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 300 && !Thread.currentThread().isInterrupted()){
if (num % 100 == 0){
System.out.println(num+"是100的倍数");
}
num++;
}
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}

结果如图所示,当线程处于sleep的时候收到中断通知,会直接抛出异常
sleep状态下收到线程中断通知

如果线程每次迭代后都堵塞

代码如下所示,在这种情况下,我们就没必须每次循环都调用Thread.currentThread().isInterrupted()去检查线程是否已经中断,因为每次循环的时候都会进行sleepsleep对中断做了处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RightWayStopThreadWithSleepEveryLoop {

public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
int num = 0;
try {
while (num <= 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}

whiletry/catch的问题

  • 如果将try/catch放到循环内部的话,中断会失败,因为上面的代码其实是依靠异常来中断了循环,然后再被循环外面的try/catch处理,而如果把try/catch放到循环里面,就无法依靠异常中断循环,所以循环将会继续
  • try/catch循环里面在while循环条件里面添加上Thread.currentThread().isInterrupted()判断是否中断,这样也是无效的,因为线程在获取到一次标记之后,就会清除标记,所以如果sleep获取了标记,Thread.currentThread().isInterrupted()就无法再成功检查了(极小概率再sleep之后,while循环之前收到中断通知,这样再进行判断的话就会中断循环,但概率极小)

实际开发中的两种最佳实践

优先选择:传递中断

如代码中所示,如果我们在调用其他方法的时候,有sleep的操作,那么不应该在方法里面捕获异常,而应该将异常上抛得到run方法里面,否则run方法里面无法感知到线程中断通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class RightWayStopThreadInProd implements Runnable{

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
@Override
public void run() {
while (true){
System.out.println("w");
try {
throwInMethod();
} catch (InterruptedException e) {
System.out.println("收到中断请求");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
}
不想或无法传递:恢复中断

如下代码所示,如果下级方法想要自己处理异常,那么可以调用Thread.currentThread().interrupt()将中断通知恢复,以便于上级run方法感应到中断通知

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
public class RightWayStopThreadInProd2 implements Runnable {

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()){
System.out.println("收到中断通知,程序运行结束");
break;
}
System.out.println("w");
reInterrupt();
}
}

private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
不应该屏蔽中断

既不在方法签名中抛出,也不在捕获异常后重新恢复中断,这样会导致上级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来停止线程,可以由被停止的线程来判断要不要停止,也可以让被停止大的线程做好一些收尾工作

停止线程的错误方法

被弃用的stopsuspendresume方法

  • stop方法会导致线程突然停止,这样对线程中运行的代码是非常不友好的,假如正在处理一个任务,想要撤销,有时候会需要对一些数据进行回滚,突然停止会导致数据无法进行处理
  • suspendresume方法,挂起和唤醒,suspend方法在挂起的时候并不会释放它已经持有大的锁,而如果需要唤醒它的线程需要获取锁才能唤醒,就会导致死锁

volatile设置boolean标记位

看上去可行(有些时候确实是可用的)

代码如下,通过更改canceled字段的值,来使线程停止

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
public class WrongWayVolatile implements Runnable{

private volatile boolean canceled = false;

@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled){
if (num % 100 == 0){
System.out.println(num + "是100的倍数");
}
num++;
Thread.sleep(1);
}
}catch (InterruptedException e){
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
WrongWayVolatile a = new WrongWayVolatile();
Thread thread = new Thread(a);
thread.start();
Thread.sleep(5000);
a.canceled = true;
}
}

错误之处(有些时候会报错)

代码如下,生产者以较快的速度生产任务,消费者以较慢的速度消费任务,当队列满了之后,就会将生产者的异步堵塞,而这个时候,即便是更改canceled字段的值,代码也无法检测到

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class WrongWayVolatileCantStop {

public static void main(String[] args) throws InterruptedException {
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);

Consumer consumer = new Consumer(storage);

while (consumer.needMoreNums()){
System.out.println(consumer.storage.take()+"被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了");
producer.canceled = true;
}
}

class Producer implements Runnable {
public volatile boolean canceled = false;
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}

@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,放入仓库了");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者停止运行");
}
}
}

class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}

修正方案

修正后的代码如下,使用interrupt()方法中断线程,并在循环的时候检查线程状态

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class WrongWayVolatileFixed {

public static void main(String[] args) throws InterruptedException {
WrongWayVolatileFixed body = new WrongWayVolatileFixed();
ArrayBlockingQueue storage = new ArrayBlockingQueue(10);
Producer producer = body.new Producer(storage);
Thread producerThread = new Thread(producer);
producerThread.start();
Thread.sleep(1000);
Consumer consumer = body.new Consumer(storage);
while (consumer.needMoreNums()){
System.out.println(consumer.storage.take()+"被消费了");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了");
producerThread.interrupt();
}
class Producer implements Runnable {
BlockingQueue storage;
public Producer(BlockingQueue storage) {
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
storage.put(num);
System.out.println(num + "是100的倍数,放入仓库了");
}
num++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者停止运行");
}
}
}
class Consumer {
BlockingQueue storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public boolean needMoreNums() {
if (Math.random() > 0.95) {
return false;
}
return true;
}
}
}

重要函数的源码解析

interrupt()方法

如图所示,interrupt()方法中的主要逻辑都是调用interrupt0()方法实现的,interrupt0()方法是一个native方法
interrupt方法代码内容
interrupt方法native内容讲解

判断是否已被中断的相关方法

static boolean interrupted()

  • 返回当前线程是否被中断,获取之后清除当前状态
  • 这个静态方法的目标对象是调用它的线程
  • 如下图,参数为true,表示要清除状态

interrupted方法代码内容

boolean isInterrupt()

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

isInterrupt方法代码内容

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class NewRunnableTerminated implements Runnable{

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new NewRunnableTerminated());
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
Thread.sleep(5);
System.out.println(thread.getState());
Thread.sleep(10);
System.out.println(thread.getState());
}

@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(i);
}
}
}
  • TIMED_WAITING -> BLOCKED -> WAITING

    线程1进入了syn()方法,执行了休眠1秒,此时的状态为 TIMED_WAITING
    因为syn()方法是synchronized修饰的,线程1进入之后,线程2无法再拿到锁,所以此时线程2的状态为 BLOCKED
    线程1休眠1秒种之后,进入wait()方法,此时状态为 WAITING

    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
    public 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());
    }

    @Override
    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相关 让线程暂停时休息和唤醒

waitnotifynotifyAll方法详解

作用,用法

阻塞阶段

  • 执行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
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
33
public class Wait {
public static Object object = new Object();
static class Thread1 extends Thread{
@Override
public void run() {
synchronized (object){
System.out.println(Thread.currentThread().getName()+"已经开始执行了");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"获取到了锁");
}
}
}
static class Thread2 extends Thread{
@Override
public void run() {
synchronized (object){
object.notify();
System.out.println("线程"+Thread.currentThread().getName()+"调用了notify()方法");
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 thread1 = new Thread1();
Thread2 thread2 = new Thread2();
thread1.start();
Thread.sleep(200);
thread2.start();
}
}

打印结果

1
2
3
Thread-0已经开始执行了
线程Thread-1调用了notify()方法
Thread-0获取到了锁

notify和notifyAll展示

代码如下所示
线程A和线程B同时启动,然后串行执行同步代码块,线程A执行到wait方法,释放锁,线程B开始执行,线程B执行到wait方法后,释放锁
等待200毫秒之后,线程C开始执行,线程C执行了notifyAll方法,唤醒了阻塞中的线程A和线程B,然后线程A和线程B执行结束
如果使用的是notify方法的话,那么线程A和线程B只能唤醒一个,另一个将进行无尽的等待

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
33
34
public class WaitNotifyAll implements Runnable {
private static final Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable r = new WaitNotifyAll();
Thread threadA = new Thread(r);
Thread threadB = new Thread(r);
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
resourceA.notifyAll();
System.out.println("线程C notifyAll");
}
}
});
threadA.start();
threadB.start();
Thread.sleep(200);
threadC.start();
}
@Override
public void run() {
synchronized (resourceA){
System.out.println(Thread.currentThread().getName()+"获取到锁");
try {
System.out.println(Thread.currentThread().getName()+"即将释放锁");
resourceA.wait();
System.out.println(Thread.currentThread().getName()+"程序即将结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

运行结果:

1
2
3
4
5
6
7
Thread-0获取到锁
Thread-0即将释放锁
Thread-1获取到锁
Thread-1即将释放锁
线程C notifyAll
Thread-1程序即将结束
Thread-0程序即将结束

只释放当前monitor展示

创建两把锁,A和B
线程1先获取锁A,然后获取锁B,然后释放锁A
线程2先获取锁A,然后尝试获取锁B
从结果可以看出,线程2是无法获取锁B的,因为线程1只释放了锁A
只释放当前monitor,不会影响已经拿到的其他monitor

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
33
34
35
36
37
38
39
40
41
42
43
public class WaitNotifyReleaseOwnMonitor {
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();

public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println("线程1获取到resourceA锁");
synchronized (resourceB) {
System.out.println("线程1获取到resourceB锁");
try {
System.out.println("线程1释放resourceA锁");
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA) {
System.out.println("线程2获取到resourceA锁");
System.out.println("线程2准备获取resourceB锁");
synchronized (resourceB) {
System.out.println("线程2获取到resourceB锁");
}
}
}
});
thread1.start();
thread2.start();
}
}

运行结果:

1
2
3
4
5
线程1获取到resourceA锁
线程1获取到resourceB锁
线程1释放resourceA锁
线程2获取到resourceA锁
线程2准备获取resourceB锁

特点,性质

  • 调用wait或者notify或者notifyAll必须先拥有monitor(否则还会抛异常:IllegalMonitorStateException)
  • notify只能唤醒一个,这一个是随机唤醒,无法指定
  • 这三个方法都属于Object类,所以所有对象都可以调用这三个方法
  • 类似功能的Condition(依赖与Lock锁)
  • 同时持有多把锁大的情况(释放锁的时候要注意是多把锁,需要避免死锁)

手写生产者消费者设计模式

代码如下所示,使用waitnotify实现生产者消费者

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable{

private EventStorage storage;

public Producer(EventStorage storage) {
this.storage = storage;
}

@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class Consumer implements Runnable{

private EventStorage storage;

public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.task();
}
}
}
class EventStorage{
private int maxSize;
private LinkedList<Date> storage;

public EventStorage() {
this.maxSize = 10;
this.storage = new LinkedList<>();
}
public synchronized void put(){
while (storage.size() == maxSize){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库里面有了 "+storage.size()+" 件产品");
notify();
}
public synchronized void task(){
while (storage.size() == 0){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了 "+storage.poll() + ",现在仓库还剩"+storage.size());
notify();
}
}

常见面试题

用程序实现两个线程交替打印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
    33
    public class WaitNotifyPrintOddEvenSyn {

    private static int count;
    private static final Object lock = new Object();
    public static void main(String[] args) {
    new Thread(new Runnable() {
    @Override
    public void run() {
    while (count < 100) {
    synchronized (lock) {
    if ((count & 1) == 0) {
    System.out.println(Thread.currentThread().getName() + ": " + count++);
    }
    }
    }
    }
    }, "偶数").start();

    new Thread(new Runnable() {
    @Override
    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
    28
    public 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{

    @Override
    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方法不释放锁

  • 包括synchronizedlock
  • wait不同

sleep方法响应中断

  • 抛出InterruptedException异常
  • 清理中断状态

    代码如下,在sleep中被中断,还会抛出异常,然后继续执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public 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();
    }
    @Override
    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
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
33
public class Join {

public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
thread1.start();
thread2.start();
System.out.println("开始等待所有子线程运行结束");
thread1.join();
thread2.join();
System.out.println("所有子线程执行完毕");
}
}

遇到中断

join抓取的中断异常是主线程的中断异常

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
public class JoinInterrupted {

public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
mainThread.interrupt();
Thread.sleep(5000);
System.out.println("子线程执行完毕");
} catch (InterruptedException e) {
System.out.println("子线程中断");
e.printStackTrace();
}
}
});
thread.start();
System.out.println("等待子线程执行完毕");
try {
thread.join();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName());
thread.interrupt();
}
System.out.println("子线程运行完毕");
}
}

在join期间,线程是什么状态?:waiting

join期间,主线程处于Waiting状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JoinThreadState {

public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread();

Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println(mainThread.getState());
System.out.println("Thread-0运行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
System.out.println("等待子线程运行");
thread.join();
System.out.println("子线程运行完毕");
}
}
  • CountDownLatch或CyclicBarrier是经过封装的,可以实现和join一样的功能

    join源码

    由图可以看出,join方法也是通过wait实现的
    而在Thread线程结束后,会调用一次notifyAll方法,唤醒线程,具体代码在C++部分
    这也不能用Thread类做锁的原因

join源码图

线程退出源码

线程退出源码

join等价代码

可以使用wait来进行替换,上面的源码已经显示,线程在退出的时候,会唤醒所有的线程,所以在线程运行完毕之后,会唤醒wait的休眠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JoinPrinciple {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});

thread1.start();
System.out.println("开始等待所有子线程运行结束");
// thread1.join();
synchronized (thread1){
thread1.wait();
}
System.out.println("所有子线程执行完毕");
}
}

面试常见问题

在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

nextThreadId

线程名字

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

线程默认名称
线程默认名称编号获取

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

设置线程名称源码

守护线程

  • 作用:给用户线程提供服务

    三个特性

  • 线程类型默认继承自父线程
  • 被谁启动(通常都是由JVM启动的)
  • 不影响JVM退出(JVM退出时只判断有没有用户线程,不会判断守护线程)

守护线程和普通线程的区别

  • 整体无区别(都是为了执行任务,不过有的是用户的任务,有的是JVM的任务)
  • 唯一的区别在于JVM的离开(用户线程会影响JVM是否退出,守护线程不会影响)

常见面试问题

  • 守护线程和普通线程的区别

    答案同上

  • 我们是否需要给线程设置为守护线程

    不应该设置为守护线程,因为如果现在在处理数据,设置成守护线程,JVM退出的时候不会考虑它是否还在运行

线程优先级

  • 优先级一共由10个级别,线程的优先级默认是5
  • 程序设计不应该依赖于优先级

    不同的操作系统对优先级的理解是不一样的(每种操作系统的优先级级别设置是不一样的,需要一些映射,这样可能会和原本的期望不一致)
    优先级会被操作系统修改()

未捕获异常如何处理?

线程未捕获异常UncaughtException应该如何处理?

  • 可以使用UncaughtExceptionHandler类来进行处理

为什么要用UncaughtExceptionHandler处理?

  • 主线程可以轻松发现异常,子线程却不行
  • 子线程的异常无法使用传统办法捕获
  • 如果不对子线程的异常进行处理,那么会导致线程中断,任务无法处理,不利于程序的健壮性

两种解决办法

  • 方案一(不推荐):手动在每个run方法里面进行try/catch
  • 方案二(推荐):利用UncaughtExceptionHandler

    UncaughtExceptionHandler接口

UncaughtExceptionHandler接口代码

异常处理器的调用策略

  • 默认线程处理器(ThreadGroup)逻辑如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public 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
    14
    public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    private String name;

    public MyUncaughtExceptionHandler(String name) {
    this.name = name;
    }

    @Override
    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
    16
    public 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();
    }
    @Override
    public void run() {
    throw new RuntimeException();
    }
    }

    常见面试问题

    Java异常体系

    如何全局处理异常?为什么要全局处理异常?不处理行不行?

    上面已经有了讲解

    run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?

  • run方法不能再向上抛出异常
  • 如果是可检查异常,只能再run方法中进行捕获处理
  • 如果是不可检查异常,抛出后,线程会终止运行

线程中如何处理某个未处理异常

  • UncaughtExceptionHandler

双刃剑:多线程会导致的问题

线程安全

什么是线程安全

不管业务中遇到怎样的多个线程访问某个对象或者方法的情况,而再编译这个业务逻辑的时候,都不需要额外等地的做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错,就可以称为线程安全)

什么情况下会出现线程安全问题,怎么避免?

运行结果错误:a++多线程下出现消失的请求现象

最终代码演示如下所示

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class MultiThreadError implements Runnable {


public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上的结果" + instance.index);
System.out.println("正在的运行次数" + realIndex.get());
System.out.println("出错的次数" + wrongCount.get());
}

int index = 0;
static final MultiThreadError instance = new MultiThreadError();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();

final static boolean[] marked = new boolean[1000000];

@Override
public void run() {
marked[0] = true;
for (int i = 0; i < 100000; i++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
index++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
realIndex.incrementAndGet();
synchronized (instance) {
if (marked[index] && marked[index - 1]) {
System.out.println("发生了错误" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
}

活跃性问题:死锁、活锁、饥饿

死锁示例代码如下

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
33
34
35
36
37
38
39
40
41
42
43
44
public class MultiThreadError implements Runnable{

int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();

public static void main(String[] args) {
MultiThreadError r1 = new MultiThreadError();
MultiThreadError r2 = new MultiThreadError();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}

@Override
public void run() {
System.out.println("flag = "+flag);
if (flag == 1){
synchronized (o1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2){
System.out.println("1");
}
}
}
if (flag == 0){
synchronized (o2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("0");
}
}
}
}
}

对象发布和初始化的时候的安全问题

什么是发布
  • 对象可以在超过当前类的范围外使用
  • public修饰的属性对象
  • 一个方法return出一个对象
  • 将一个对象作为参数传到其他类的方法中
什么是逸出
  • 1.方法返回一个private对象(private的本意是不让外部访问)
  • 2.还未完成初始化(构造函数没有完全执行完毕)就把对象提供给外界,比如:
    • 在构造函数中未初始化完毕就this赋值
    • 隐式逸出——注册监听事件
    • 构造函数中运行线程

各种需要考虑线程安全的情况

  • 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
  • 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check0then-act
  • 不同的数据之间存在捆绑关系的时候
  • 我们使用其他类的时候,如果对方没有声明自己是线程安全的

为什么多线程会带来性能问题

调度:上下文切换

  • 主要是值当线程发生调度的时候
  • 当可运行的线程数超过了CPU的核数,就会开始调度线程,以便于让每个线程都有机会运行

什么是上下文

  • 主要和一些程序计数器相关
  • 切换的时候首先需要挂起一个线程,将线程的运行状态保存得到内存中的某处
  • 包括线程运行时需要的一些数据

缓存开销

  • CPU为了运行速度,会先在通过计算,在内存中缓存一些可能用到的数据
  • 而进行了上下文切换之后,这些缓存的数据大部分都已经没用了

何时会导致密集的上下文切换

  • 频繁的抢锁,频繁的IO操作

协作:内存同步

  • 线程在运行的时候,会在线程内部缓存一些数据,如果使用锁,会强制同步线程的缓存数据和主内存的数据
  • 使线程缓存数据失效,只能使用主内存数据