多线程三之线程8大核心基础

线程8大核心基础

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

网上说法

1种的观点,2种的观点,4种的观点,其他观点

正确说法

Oracle官网的文档是如何写的?

正确答案是2种

1
There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started.
1
The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started.

方法一

实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 描述:用Runnable方式创建线程
*/
public class RunnableStyle implements Runnable{

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

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

输出结果

1
用Runnable方法实现线程

方法二

继承Thread类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 描述:用Thread方式实现线程
*/
public class ThreadStyle extends Thread{

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

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

输出结果

1
用Thread类实现线程

两种方法的对比

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

  1. 从代码架构去考虑,具体的执行任务(run方法)应该与线程的创建与运行的机制(也就是Thread类)是解耦的,不应该把这两件事混为一坛,从解耦的角度实现Runnable接口更好
  2. 继承Thread类每次想新建一个任务,只能新建一个独立的线程,而新建一个独立的线程是十分消耗资源的,需要创建、执行、销毁,而使用Runnable就可以使用后续的像线程池这样的工具来减少这些资源的损耗
  3. 因为Java不支撑双继承,导致一个类无法继承多个类,这样就限制了可扩展性

两种方法的本质区别

方法一(实现Runnable接口):最终调用target.run();

当实现Runnable接口的时候,实际是传入一个target对象,在执行run()方法时候由于target不为null就执行了target.run()方法

1
2
3
4
//Thread构造方法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
1
2
//Thread类中的target变量
private Runnable target;
1
2
3
4
5
6
7
//Thread类中的run()方法
@Override
public void run() {
if (target != null) {
target.run();
}
}

方法二(继承Thread类):run()整个都被重写

继承Thread类,并重写run()方法,一旦重写,run()中的方法就不会被使用,而是重写新的方法,将只执行run()方法里的内容,其他不会执行

1
2
3
4
@Override
public void run() {

}

思考题

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 描述:同时使用Runnable和Thread两种实现线程的方式
*/
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();
}
}

输出结果

1
我来自Thread

从面向对象的思想去考虑

1
2
3
4
@Override
public void run() {
System.out.println("我来自Thread");
}

覆盖了之前的的run()方法

1
2
3
4
@Override
public void run() {
System.out.println("我来自Runnable");
}

所以输出结果就是

1
我来自Thread

总结

最精准的描述

通常我们可以分为两类,Oracle也是这么说

准确的讲,创建线程只有一种方式那就是构造Thread类,而实现线程的执行单元有两种方式

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

经典错误观点

“线程池创建线程也是一种新建线程的方式” 错误

1
2
3
4
5
6
7
8
9
10
public class ThreadPool5 {

public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
executorService.submit(new Task() {
});
}
}
}

newCachedThreadPool方法

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

newCachedThreadPool方法

1
2
3
4
5
6
7
8
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}

defaultThreadFactory方法

1
2
3
public static ThreadFactory defaultThreadFactory() {
return new DefaultThreadFactory();
}

DefaultThreadFactory方法

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
/**
* The default thread factory
*/
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;

DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}

//重点在这里,要注意
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}

从这里看出

1
2
3
4
5
6
7
8
9
10
11
//重点在这里,要注意
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}

线程池同样是通过Runnable接口实现

“通过Callable和FutureTask创建线程,也算是一种新建线程的方式” 错误

“无返回值是实现runnable接口,有返回值是实现callable接口,所以callable是新的实现线程的方式”错误

此方法与上面类似

Callable接口

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}

FutureTask构造函数中传入Callable

1
2
3
4
5
6
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}

FutureTask中的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
30
31
32
33
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
//看这里,注意
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

定时器 错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 描述:定时器创建线程
*/
public class DemoTimmerTask {

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

匿名内部类 错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 描述: 匿名内部类的方式
*/
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();
}
}

Lambda表达式 错误

1
2
3
4
5
6
7
8
9
/**
* 描述: lambda表达式创建线程
*/
public class Lambda {

public static void main(String[] args) {
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
}
}

总结:

多线程的实现方式,在代码中写法千变万化,但是本质万变不离其宗

彩蛋

如何从宏观和微观两个方面来提高技术

宏观上

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

微观上

  1. 看经典书籍(指外国人写的经典的中国译本,比如所Java并发编程实战、自定向下计算机网络)

  2. 看官方文档

  3. 英文搜google和stackoverflow

  4. 自己动手写,实践demo,尝试用到项目里

  5. 不理解的领域参考多个书本,综合判断

  6. 学习开源项目,分析源码

如何了解技术领域的前沿动态

高质量固定途径:ohmyrss.com(信息源筛选,为我所用)

订阅技术网站的邮件:InfoQ(每周都看)

常见面试题

面试第一题

有多少种实现线程的方法?

思路有5点

  1. 从不同角度看,会有不同的答案
  2. 典型答案是两种
  3. 我们看原理,两种本质都是一样的
  4. 具体展开说
  5. 结论

面试第二题

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

  1. 从代码架构角度
  2. 新建线程的损耗
  3. Java不支持双继承

启动线程的正确和错误方式

start()和run()的比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 描述:对比start和run两种启动线程的方式
*/
public class StartAndRunMethod {

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

第一个输出的结果为main说明是主线程执行的run()方法中内部的代码,第二个输出结果为Thread-0说明是子线程Thread-0输出的结果

start()方法原理解读

start()方法含义12

1启动新线程

线程对象在初始化之后调用start()方法,于是当前线程(通常是主线程)告诉JVM虚拟机,如果有空闲时间,就执行新的线程,至于何时能运行,并不是我们能决定的,而是由线程调度器决定的,即start()方法运行之后并不能确定线程已经运行了,可能稍后运行,也可能很长时间之后都不会运行(遇到饥饿的情况)

我们调用线程的顺序并不能决定线程执行的顺序,只是告诉JVM在合适的时候来启动

start()方法其实是父线程(主线程)执行的,start()方法被父线程(主线程)执行之后才会创建新线程

2start()方法的准备工作

start()方法被父线程(主线程)执行之后才会创建新线程,而新创建出来的线程需要做一些准备工作才能去执行,

首先新线程会处于就绪状态,即已经获取到除了CPU以外的所有资源

不能重复执行start()方法
1
2
3
4
5
6
7
8
9
/**
* 描述:演示不能两次调用start方法,否则会报错
*/
public class CantStartTwice {
public static void main(String[] args) {
Thread thread = new Thread();
thread.start();
thread.start();
}
1
2
3
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at threadcoreknowledge.startthread.CantStartTwice.main(CantStartTwice.java:10)

源码解析

1启动新线程检查线程状态

1
2
3
4
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
private volatile int threadStatus = 0;
1
2
if (threadStatus != 0)
throw new IllegalThreadStateException();

2加入线程组

1
group.add(this);

3调用start0()

start0()方法是native底层方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
1
private native void start0();

run()方法原理解读

1
2
3
4
5
public void run() {
if (target != null) {
target.run();
}
}

常见面试题

1一个线程两次调用start()方法会出现什么情况?为什么?

2既然start()方法会调用run()方法,为什么选择调用start()方法,而不是直接调用run()方法?

start()方法才是真正意义上的启动一个线程,才会经历线程生命的各个周期,而直接调用run()方法,它就是一个普通的方法,也不会用子线程调用

如何正确停止线程

原理介绍

使用interrupet来通知,而不是强制,将停止线程的权利交个被停止的线程本身

最佳实践:如何正确停止线程

通常线程会在什么情况下停止

  1. 包含run()方法的所有方法都执行完毕,正常执行完
  2. 有异常出现,并且没有进行捕获,就会出现停止

正确的停止方式:使用interrupt

使用interrupt的几种情况

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
/**
* 描述:run方法内没有sleep或wait方法时,停止线程
*/
public class RightWayStopThreadWithoutSleep implements Runnable {
//打印出最大整数一半以内所有是10000的倍数
@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("任务运行结束了");
}

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

在执行interrupt的同时还要添加!Thread.currentThread().isInterrupted()才能真正的执行终止操作

2线程被阻塞的时候停止线程

在线程sleep的时候中断线程

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
/**
* 描述:带有sleep的中断线程的写法,打出100的倍数,打出几个100的倍数之后,该线程进入等待,在等待的时候突 * 然被其他的线程中断
*/
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();
}
}

输出结果

1
2
3
4
5
6
7
8
0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at threadcoreknowledge.stopthreads.RightWayStopThreadWithSleep.lambda$main$0(RightWayStopThreadWithSleep.java:18)
at java.lang.Thread.run(Thread.java:748)
3如果线程在每次迭代的时候都阻塞

Thread.sleep(10)阻塞的过程中,在阻塞的过程中收到终止线程,此时就会抛出异常

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
/**
* 描述:如果在执行过程中,每次循环都会调用sleep或wait等方法,那么不需要每次迭代都检查是否已中断
*/
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();
}
}

输出结果

1
2
3
4
5
6
7
8
0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at threadcoreknowledge.stopthreads.RightWayStopThreadWithSleepEveryLoop.lambda$main$0(RightWayStopThreadWithSleepEveryLoop.java:16)
at java.lang.Thread.run(Thread.java:748)
4while内try/catch的问题

如果while里面放try/catch,会导致中断失效

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
/**
* 描述:如果while里面放try/catch,会导致中断失效
*/
public class CantInterrupt {

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

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
0是100的倍数
100是100的倍数
200是100的倍数
300是100的倍数
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at threadcoreknowledge.stopthreads.CantInterrupt.lambda$main$0(CantInterrupt.java:17)
at java.lang.Thread.run(Thread.java:748)
400是100的倍数
500是100的倍数
600是100的倍数
700是100的倍数
800是100的倍数

Java语言在设计sleep函数的时候有这样一个理念,就是当线程响应中断的时候,便会把线程的interrupt的标志位清除

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

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

@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("go");
throwInMethod();
}
}

private void throwInMethod() {
//注意这里try/catch语句,异常要抛到run()方法层
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

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

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
go
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at threadcoreknowledge.stopthreads.RightWayStopThreadInProd.throwInMethod(RightWayStopThreadInProd.java:20)
at threadcoreknowledge.stopthreads.RightWayStopThreadInProd.run(RightWayStopThreadInProd.java:14)
at java.lang.Thread.run(Thread.java:748)
go
go
go
go
go
...

因为数据太多导致很难注意到错误的结果,即使添加Thread.currentThread().isInterrupted()也无法进行中断,throwInMethod()把异常给吞了,没有进行上报处理,没有交给用户

正确情形:

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
/**
* 描述:最佳实践:catch了InterruptedExcetion之后的优先选择在方法签名中抛出异常
* 如果这样做那么在run()就会强制对异常进行try/catch
* 方法声明的两个组件构成了方法签名 - 方法的名称和参数类型
*/
public class RightWayStopThreadInProd implements Runnable {

@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("go");
//注意这里,抛出的异常应该在run()方法中
try {
throwInMethod();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
//保存日志、停止程序
//以这种方式可以正确的处理响应中断请求的,如果被中断,就可以按照我们的业务逻辑去进行响应
System.out.println("保存日志");
e.printStackTrace();
}
}
}

//注意这里,如果要抛出异常,最好把异常抛出去,由顶层的调用方进行处理
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}

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

注意:

在run()中出现的异常最高只能在run()方法中进行处理,而不能继续向外抛,即在run()方法中只能使用try/catch而不能使用throw Exception

此时就会抛出异常

image-20200304212936052

在run()方法中进行try/carch处理,就不会抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 描述:run无法抛出checked Exception,只能用try/catch
*/
public class RunThrowException {

public void aVoid() throws Exception {
throw new Exception();
}

public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
}

2不想或无法传递异常

恢复中断

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
/**
* 描述:最佳实践2:在catch子语句中调用Thread.currentThread().interrupt()来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断
* 回到刚才RightWayStopThreadInProd补上中断,让它跳出
*/
public class RightWayStopThreadInProd2 implements Runnable {

@Override
public void run() {
while (true) {
//此时要进行是否被中断的检测
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted,程序运行结束");
break;
}
reInterrupt();
}
}

private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
//注意这里
//抛出异常的时候重新设置中断,没有自我独吞,而是重新抛了出来
Thread.currentThread().interrupt();
e.printStackTrace();
}
}

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

输出结果

1
2
3
4
5
6
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at threadcoreknowledge.stopthreads.RightWayStopThreadInProd2.reInterrupt(RightWayStopThreadInProd2.java:22)
at threadcoreknowledge.stopthreads.RightWayStopThreadInProd2.run(RightWayStopThreadInProd2.java:16)
at java.lang.Thread.run(Thread.java:748)
Interrupted,程序运行结束

在获取到interrupt的时候应该在catch语句中再次调用Thread.currentThread().interrupt() ,这样就把中断重新设置了一遍,在后续的执行中就能检测出后续的中断,并由后续的逻辑继续处理

3不应屏蔽中断

此方法是最不应该做的,既不在方法中抛出,也不在catch语句中重新恢复中断,这样就屏蔽了中断请求,这绝对不是什么好事

响应中断的方法总结列表

如果一个中断信号过来,wait/sleep/join是可以感知到的,这就是拥有响应中断的能力

一下这10种情况都是可以响应中断的,如果通过这10个方法使线程进入阻塞状态,我们又想让这些线程从阻塞状态恢复,就可以使用interrupt()方法来及时的中断线程

1
2
3
Object.wait()/wait(long)/wait(long,int)
Thread.sleep(long)/sleep(long,int)
Thread.join()/join(long)/join(long,int)
1
2
3
4
5
6
7
java.util.concurrent.BlockingQueue.take()/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()方法,而是使用interrupt()方法来发出一个信号,让它们自己来处理,这样会更安全

错误的停止方法

1被弃用的stop方法

正常情况

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 StopThread implements Runnable {

@Override
public void run() {
//模拟指挥军队:一共有5个连队,每个连队10人,以连队为单位,发放武器弹药,叫到号的士兵前去领取
for (int i = 0; i < 5; i++) {
System.out.println("连队" + i + "开始领取武器");
for (int j = 0; j < 10; j++) {
System.out.println(j);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("连队"+i+"已经领取完毕");
}
}

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

输出结果

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
连队0开始领取武器
0
1
2
3
4
5
6
7
8
9
连队0已经领取完毕
连队1开始领取武器
0
1
2
3
4
5
6
7
8
9
连队1已经领取完毕
连队2开始领取武器
0
1
2
3
4
5
6
7
8
9
连队2已经领取完毕
连队3开始领取武器
0
1
2
3
4
5
6
7
8
9
连队3已经领取完毕
连队4开始领取武器
0
1
2
3
4
5
6
7
8
9
连队4已经领取完毕

错误情况

用stop()来停止线程,会导致线程运行一半突然停止,没办法完成一个基本单位的操作(一个连队),会造成脏数据(有的连队多领取少领取装备)

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
/**
* 描述:错误的停止方法:用stop()来停止线程,会导致线程运行一半突然停止,没办法完成一个基本单位的操作(一
*个连队),会造成脏数据(有的连队会多领取或少领取装备)
*/
public class StopThread implements Runnable {

@Override
public void run() {
//模拟指挥军队:一共有5个连队,每个连队10人,以连队为单位,发放武器弹药,叫到号的士兵前去领取
for (int i = 0; i < 5; i++) {
System.out.println("连队" + i + "开始领取武器");
for (int j = 0; j < 10; j++) {
System.out.println(j);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("连队"+i+"已经领取完毕");
}
}

public static void main(String[] args) {
Thread thread = new Thread(new StopThread());
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.stop();
}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
连队0开始领取武器
0
1
2
3
4
5
6
7
8
9
连队0已经领取完毕
连队1开始领取武器
0
1
2
3
4
5
6
7
8

连队1的9号士兵没有领取到武器,系统会认为连队0和连队1的所有士兵都领取完了,这就是使用stop带来的数据错乱

Thread.stop本质上是不安全的,但停止线程会导致它释放之前已经锁定的所有监视器(锁)

2suspend和resume方法

suspend将线程挂起,运行->阻塞,调用后并不释放所占用的锁

resume将线程解挂,阻塞->就绪

suspend并不会破坏,但是suspend和resume不会释放锁,是带着锁进行休息的,这样容易造成死锁

1独占:因为suspend在调用过程中不会释放所占用的锁,所以如果使用不当会造成对公共对象的独占,使得其他线程无法访问公共对象,严重的话造成死锁

2不同步:容易出现因线程暂停导致的数据不同步

3用volatile设置boolean标记位

看上去可行
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
/**
* 描述:演示用volatile的局限:part1 看似可行
*/
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 r = new WrongWayVolatile();
Thread thread = new Thread(r);
thread.start();
Thread.sleep(5000);
r.canceled = 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
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
71
72
73
/**
* 描述: 演示用volatile的局限part2 陷入阻塞时,volatile是无法线程的
*/
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;
System.out.println(producer.canceled);
}
}

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;
}
}
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
0是100的倍数,被放到仓库中了。
100是100的倍数,被放到仓库中了。
200是100的倍数,被放到仓库中了。
300是100的倍数,被放到仓库中了。
400是100的倍数,被放到仓库中了。
500是100的倍数,被放到仓库中了。
600是100的倍数,被放到仓库中了。
700是100的倍数,被放到仓库中了。
800是100的倍数,被放到仓库中了。
900是100的倍数,被放到仓库中了。
1000是100的倍数,被放到仓库中了。
0被消费了
1100是100的倍数,被放到仓库中了。
100被消费了
1200是100的倍数,被放到仓库中了。
200被消费了
1300是100的倍数,被放到仓库中了。
300被消费了
1400是100的倍数,被放到仓库中了。
400被消费了
500被消费了
1500是100的倍数,被放到仓库中了。
1600是100的倍数,被放到仓库中了。
600被消费了
700被消费了
1700是100的倍数,被放到仓库中了。
1800是100的倍数,被放到仓库中了。
800被消费了
900被消费了
1900是100的倍数,被放到仓库中了。
2000是100的倍数,被放到仓库中了。
1000被消费了
2100是100的倍数,被放到仓库中了。
1100被消费了
1200被消费了
2200是100的倍数,被放到仓库中了。
2300是100的倍数,被放到仓库中了。
1300被消费了
1400被消费了
2400是100的倍数,被放到仓库中了。
2500是100的倍数,被放到仓库中了。
1500被消费了
2600是100的倍数,被放到仓库中了。
1600被消费了
1700被消费了
2700是100的倍数,被放到仓库中了。
2800是100的倍数,被放到仓库中了。
1800被消费了
1900被消费了
2900是100的倍数,被放到仓库中了。
3000是100的倍数,被放到仓库中了。
2000被消费了
3100是100的倍数,被放到仓库中了。
2100被消费了
消费者不需要更多数据了。
true

最后的时候canceled已经是true了,但是线程没有停止下来,根本没有执行finally

image-20200305131453211

一旦遇到线程长时间阻塞,这种办法就失效了,这是因为每一次 在执行while (num <= 100000 && !canceled)的时候才能检查canceled是否为true,而循环体的里面是没有检查是否阻塞的逻辑的,而代码是阻塞在storage.put(num)这里的,没有线程唤醒,自然不能继续执行,走到下一个while (num <= 100000 && !canceled),所以就会不停的等待

修正方案

使用interrupt进行终止线程

如下使用ArrayBlockingQueue队列实现生产者消费者模式

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
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;
}
}
}

输出结果

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
0是100的倍数,被放到仓库中了。
100是100的倍数,被放到仓库中了。
200是100的倍数,被放到仓库中了。
300是100的倍数,被放到仓库中了。
400是100的倍数,被放到仓库中了。
500是100的倍数,被放到仓库中了。
600是100的倍数,被放到仓库中了。
700是100的倍数,被放到仓库中了。
800是100的倍数,被放到仓库中了。
900是100的倍数,被放到仓库中了。
0被消费了
1000是100的倍数,被放到仓库中了。
1100是100的倍数,被放到仓库中了。
100被消费了
200被消费了
1200是100的倍数,被放到仓库中了。
300被消费了
1300是100的倍数,被放到仓库中了。
1400是100的倍数,被放到仓库中了。
400被消费了
1500是100的倍数,被放到仓库中了。
500被消费了
1600是100的倍数,被放到仓库中了。
600被消费了
700被消费了
1700是100的倍数,被放到仓库中了。
1800是100的倍数,被放到仓库中了。
800被消费了
1900是100的倍数,被放到仓库中了。
900被消费了
1000被消费了
2000是100的倍数,被放到仓库中了。
消费者不需要更多数据了。
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2048)
at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:353)
at threadcoreknowledge.stopthreads.volatiledemo.WrongWayVolatileFixed$Producer.run(WrongWayVolatileFixed.java:47)
at java.lang.Thread.run(Thread.java:748)
生产者结束运行

停止线程相关的重要函数解析

static boolean interrupted()

检测当前线程是否被中断,同时会把线程的中断状态直接设置为true,也就是将线程中断状态直接清除,这也是唯一能清除线程状态的办法,如果返回true,实际上是帮助我们清除了线程中断状态,此时要对这种行为做出处理,无论是抛出InterruptedException或者是再次执行interrupted来中断线程都可以

boolean isInterrupted()

检测当前线程是否被中断,但不会把线程的中断状态清除

Thread.interrupted()的目标对象

Thread.interrupted()方法的目标对象是当前执行它的线程,当前运行它的线程,而不管本方法来自于哪个对象

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
/**
* 描述:注意Thread.interrupted()方法的目标对象是“当前线程”,而不管本方法来自于哪个对象
*/
public class RightWayInterrupted {

public static void main(String[] args) throws InterruptedException {

Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {
for (; ; ) {
}
}
});

// 启动线程
threadOne.start();
//设置中断标志
//返回true
threadOne.interrupt();
//获取中断标志
//返回true
System.out.println("isInterrupted: " + threadOne.isInterrupted());
//获取中断标志并重置
//执行这个线程的是main方法,main方法没有被中断,所以应该为false
System.out.println("isInterrupted: " + threadOne.interrupted());
//获取中断标志并重直
//不管是Thread还是threadOne,都返回当前主线程的状态,所以应该返回false
System.out.println("isInterrupted: " + Thread.interrupted());
//获取中断标志
//返回true
System.out.println("isInterrupted: " + threadOne.isInterrupted());
threadOne.join();
System.out.println("Main thread is over.");
}
}
1
2
3
4
isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true

常见面试题

1如何停止线程

  1. 原理:用interrupt来请求、好处

  2. 想停止线程、要请求方、被请求方、子方法被调用方相互配合

    请求方发出终止线程的信号,被请求方必须在每次循环或者适当的时候去检查这个信号,在可能抛出interruptedException的时候去处理这个信号,每一个线程都应该去做这样的事情,以便于自己能够停止

    如果我们是去写子方法的,这个子方法是会被我们的线程所调用的,那么有两点最佳实践,优先是在子方法中抛出这个异常,以便于其他人进行处理,或者收到异常之后,再次设置中断状态

    如果这三方处理得当,就会得到完美的处理结果

  3. 最后再说错误的方法:stop/suspend已废弃,volatile的boolean无法处理长时间阻塞的情况

2如何处理不可中断的阻塞

没有通用的解决方案,要针对不同的锁给出不同的解决方案

线程的6个状态(生命周期)

线程的6种状态

每个状态的含义

New

已创建但还没有启动的线程,new Thread()新建线程之后没有执行start()方法,此时线程处于New的状态

Runnable

可运行线程的线程状态。这里其实合并了两种状态(RUNNING、RUNABLE)

翻译过来为可运行的

从New状态调用start()方法就进入Runnable状态,Java中的Runnable状态实际上对应着操作系统中的两种状态,分别是Ready(可运行的)和Running(运行中的),此时的状态既可以表示可运行的,可运行的就是其他资源都准备好了,但是还差CPU资源去执行。也可以表示实际运行中的

注意:

线程的得到CPU资源时是Runnable状态,但是CPU资源是不停的被调度的,有的时候CPU资源会突然被拿走,此时这个线程还是Runnable状态,因为即使没有CPU资源,也是处于可运行的状态,这也是Runnable状态

Blocked

针对Blocked状态一定是针对synchronized修饰的,无论是synchronized修饰的代码块还是方法都可以

在synchronized修饰的代码中,且没有得到锁而陷入等待状态,这种情况下就是Blocked状态

Waiting

等待状态,表示线程进入状态。进入此状态后,会无限等待,直到其他线程做出一些特定的动作(唤醒通知、中断通知)才会再次运行

Timed Waiting

计时等待状态,此状态与 WAITING 状态有些类似,但它是有时间限制的,即只会等待一段指定的时间,当时间到来前,没有被唤醒或或中断,那么时间到来了,就自动”醒来”,进入RUNNABLE状态

Terminated

终止状态,已终止线程的线程状态

注意

  1. 当线程的run方法结束时,该线程就完成。即线程死亡。但注意,此时线程的状态是死亡了,而且是不可以复活的,但是死亡的线程的对象并没有立即消失(因为Thread是一个类,是一个记录、操作线程的类),特别是在别处被引用下,你可以继续调用这个Thread实例上的大部分方法,而对线程操作的方法基本上都会抛异常,如:start()、wait()、notify()不可以再调用
  2. 只要线程启动了,也就是调用start()方法,也就永远就不能再次启动
  3. 线程执行的顺序与线程启动的顺序无关,start()线程启动,线程首先由新建状态变成就绪状态,并不是直接就是运行状态,即不会马上就运行,何时进入CPU运行,得看调度算法
  4. java 将操作系统当中的就绪 与 运行两个状态合并为运行状态
  5. 线程进入synchronize修饰的方法或代码块中,线程的状态变为阻塞状态。但如果线程进入的是Lock接口的代码块中,却是等待状态。这是因为Lock对于阻塞的本质实现是使用了LockSupport类中的相关方法
  6. WAITING 、TIMED_WAITING 两种等待状态都是可以被”中断”打断的,所以那些将线程变为等待状态的方法,如wait()、sleep()等都要捕获 InterruptedException异常

状态间的转化图示

状态转化的特殊情况

  • 从Object.wait()状态刚被唤醒时,通常不能立即抢到monitor锁,那就会从Waiting先进入Blocked状态,抢到锁后再转换到Runnable状态(官网文档)

    1
    2
    public static final Thread.State BLOCKED
    Thread state for a thread blocked waiting for a monitor lock. A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method or reenter a synchronized block/method after calling Object.wait
  • 如果发生异常,可以直接跳到终止Terminated状态,不必再遵循路径,比如可以从Waiting直接到Terminated

展示线程的NEW、RUNNABLE、Terminated状态

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
/**
* 描述:展示线程的NEW、RUNNABLE、Terminated状态。即使是正在运行,也是Runnable状态,而不是Running。
*/
public class NewRunnableTerminated implements Runnable {

public static void main(String[] args) {
Thread thread = new Thread(new NewRunnableTerminated());
//打印出NEW的状态
System.out.println(thread.getState());
thread.start();
//打印出Runnable
System.out.println(thread.getState());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印出RUNNABLE的状态,即使是正在运行,也是RUNNABLE,而不是RUNNING
System.out.println(thread.getState());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印出TERMINATED状态
System.out.println(thread.getState());
}

@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(i);
}
}
}

展示Blocked, Waiting, TimedWaiting

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
/**
* 描述:展示Blocked, Waiting, TimedWaiting
*/
public class BlockedWaitingTimedWaiting implements Runnable{
public static void main(String[] args) {
BlockedWaitingTimedWaiting runnable = new BlockedWaitingTimedWaiting();
Thread thread1 = new Thread(runnable);
thread1.start();
Thread thread2 = new Thread(runnable);
thread2.start();
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印出Timed_Waiting状态,因为正在执行Thread.sleep(1000);
System.out.println(thread1.getState());
//打印出BLOCKED状态,因为thread2想拿得到sync()的锁却拿不到
System.out.println(thread2.getState());
try {
Thread.sleep(1300);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印出WAITING状态,因为执行了wait()
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类中的重要方法解析

考一考

为什么线程通信的方法wait()、notify()和notifyAll()被定义在Object类里?而sleep()定义在Thread类里?

用3中方式实现生产者模式

JavaSE8和Java1.8和JDK8是什么关系,是同一个东西吗?

join和sleep和wait期间线程的状态分别是什么?为什么?

方法概览

方法名 简介
Thread sleep相关 本表格的”相关“,指的是重载方法,也就是方法名相同,但是参数不同,例如sleep有多个方法,只是参数不同,实际作用大同小异
join 等待其他线程执行完毕
yield相关 方法已经获取到的CPU资源
currentThread 获取当前执行线程的引用
start、run相关 启动线程相关
interrupt 中断线程
stop、suspend、resume相关 已废弃
Object wait、notify、notifyAll相关 让线程暂时休息和唤醒

wait、notify、notifyAll方法详解

作用与用法

用法主要分为3个阶段,即阻塞阶段、唤醒阶段、遇到中断

阻塞阶段

有的时候想要一个或多个线程暂时休息一下,等到我们后续需要的时候或者条件成熟的时候再去唤醒它,这就是wait、notify、notifyAll的作用,即它可以让我们控制其他线程去休息以及被唤醒,一旦执行了休息就意味着进入到了阻塞阶段。

关于阻塞阶段要注意,它执行wait方法的时候必须先拥有这个对象的monitor锁,调用wait方法首先是一个线程调用,调用它的对象自己就会进入阻塞状态,不再参与线程的调度。

直到以下4种情况之一发生,才会被唤醒

  1. 另一个线程调用这个对象的notify()方法且刚好被唤醒的是本线程
  2. 另一个线程调用这个对象的notifyAll()方法
  3. 过了wait(long timeout)规定的超时时间。如果传入0就是永久等待
  4. 线程自身调用了interrupt()

唤醒阶段

notify会唤醒单个正在等待monitor的线程,如果有多个线程在等待,则只会选取其中一个,而具体的唤醒的选择是任意的,java对此没有一个严格的规范

notify、wait、notifyAll他们都需要在synchronized代码块或方法中执行,如果在synchronized外面执行会抛出异常

一旦被唤醒,则线程不再是等待的状态,当前线程就会重新参与线程的调度当中,等待时机合适就会被继续执行

notifyAll会把等待的线程全部唤醒

遇到中断

若线程执行了wait方法,如果被中断,则会抛出InterrupterException,并且释放掉当前已经获取的monitor(锁)

代码演示

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
34
35
36
37
38
39
40
41
42
43
44
/**
* 描述:展示wait和notify的基本用法
* 1. 研究代码执行顺序
* 2. 证明wait释放锁
*/
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获取到了锁。

2notify和notifyAll的用法

start先执行不代表线程先启动

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
/**
* 描述:3个线程,线程1和线程2首先被阻塞,线程3唤醒它们
* notify, notifyAll
*/
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("ThreadC notified.");
}
}
});
threadA.start();
threadB.start();
Thread.sleep(200);
threadC.start();
}
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName()+" got resourceA lock.");
try {
System.out.println(Thread.currentThread().getName()+" waits to start.");
resourceA.wait();
System.out.println(Thread.currentThread().getName()+"'s waiting to end.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

输出结果

image-20200305222403686

注意此方法的运算结果在执行完的时候终止了

第一个注意点

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
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.notify();
System.out.println("ThreadC notified.");
}
}
});
threadA.start();
threadB.start();
Thread.sleep(200);
threadC.start();
}
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName()+" got resourceA lock.");
try {
System.out.println(Thread.currentThread().getName()+" waits to start.");
resourceA.wait();
System.out.println(Thread.currentThread().getName()+"'s waiting to end.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

输出结果

可以看出程序没有停止,而是一直在执行,因为notify()只能唤醒一个线程,只唤醒了线程0,却不能唤醒线程1

image-20200305223245960

第二个注意点

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
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("ThreadC notified.");
}
}
});
threadA.start();
threadB.start();
//注意这里
//Thread.sleep(200);
threadC.start();
}
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName()+" got resourceA lock.");
try {
System.out.println(Thread.currentThread().getName()+" waits to start.");
resourceA.wait();
System.out.println(Thread.currentThread().getName()+"'s waiting to end.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

输出结果

image-20200306075851814

线程的执行顺序与代码位置无关,不休眠就无法保证线程的执行顺序,如图,线程0获取锁之后进入等待,线程C就获取到了锁并唤醒,时候线程1获取到锁并进入等待,但是却没有线程去唤醒它,它就进入无尽的等待

3只释放当前monitor的用法

执行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
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
/**
* 描述:证明wait只释放当前的那把锁
*/
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("ThreadA got resourceA lock.");
synchronized (resourceB) {
System.out.println("ThreadA got resourceB lock.");
try {
System.out.println("ThreadA releases resourceA lock.");
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("ThreadB got resourceA lock.");
System.out.println("ThreadB tries to resourceB lock.");

synchronized (resourceB) {
System.out.println("ThreadB got resourceB lock.");
}
}
}
});

thread1.start();
thread2.start();
}
}

输出结果

1
2
3
4
5
ThreadA got resourceA lock.
ThreadA got resourceB lock.
ThreadA releases resourceA lock.
ThreadB got resourceA lock.
ThreadB tries to resourceB lock.

wait、notify、notifyAll特点

  1. 使用之前必须先拥有monitor,否则会抛出异常

  2. notify只会唤醒一个,并且是随机的,我们无法预知

  3. 这三个方法都属于Object类的

  4. jdk已经帮忙封装好一个类似的功能Condition,Condition和wait、notify、notifyAll的功能非常类似

  5. 同时持有多个锁时,释放锁只会释放wait对应的对象的那一把锁,如果一个线程同时持有多把锁,要注意释放的顺序与获取的顺序非常重要,可能会导致死锁的发生

wait原理

这个图想表达的是抢synchronized锁的一系列动作,在抢锁的过程中,从1开始逐步的往下,首先开始抢锁,于是进入Entry Set入口集,在入口集可能有多个线程,其中一个又一个圆圈代表线程,执行到3释放锁进入等待集,在Set区域里的线程都在等待其他线程对锁的释放,然后它们再来抢锁

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

为什么要使用生产者和消费者模式

生产者为生产数据,消费者为使用数据,但是它们的速度很有可能是不一致的,有的时候生产者比较块,有的时候生产者比较慢,如果能有一个设计模式解决它们之间相互等待的问题,而不至于一个过快一个过慢使得相互配合变得困难,就需要使用这样的设计模式

生产者消费者设计模式实际上是把消费方和生产方进行了解耦,通过解耦可以达到更加流畅的配合

如图所示

首先生产者消费者会利用一个容器(通常是阻塞队列)来解决它们之间的耦合问题,有了中间的媒介,左边是消费者,右边是生产者,它们之间不需要直接通信,所有的通信都是通过中间的队列,右边生产对象之后放在队列里,而左边从队列中获取对象,这样就有了缓冲区,这就把生产者消费者之间的能力进行了平衡

当生产者发现队列满了,它就停止生产,并通知消费者消费,因为中间的队列是有一定容量的,左边的消费者发现队列满了,就停止消费,并通知消费者继续生产

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
71
72
73
74
75
76
77
78
79
80
/**
* 描述:用wait/notify来实现生产者消费者模式,不允许使用阻塞队列
*/
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.take();
}
}
}

class EventStorage {

private int maxSize;
private LinkedList<Date> storage;

public EventStorage() {
maxSize = 10;
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 take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
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
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
仓库里有了1个产品。
仓库里有了2个产品。
仓库里有了3个产品。
仓库里有了4个产品。
仓库里有了5个产品。
仓库里有了6个产品。
仓库里有了7个产品。
仓库里有了8个产品。
仓库里有了9个产品。
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下8
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下7
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下6
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下5
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下4
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下3
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下2
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下1
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
仓库里有了2个产品。
仓库里有了3个产品。
仓库里有了4个产品。
仓库里有了5个产品。
仓库里有了6个产品。
仓库里有了7个产品。
仓库里有了8个产品。
仓库里有了9个产品。
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下8
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下7
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下6
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下5
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下4
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下3
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下2
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下1
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
仓库里有了2个产品。
仓库里有了3个产品。
仓库里有了4个产品。
仓库里有了5个产品。
仓库里有了6个产品。
仓库里有了7个产品。
仓库里有了8个产品。
仓库里有了9个产品。
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下8
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下7
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下6
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下5
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下4
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下3
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下2
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下1
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
仓库里有了2个产品。
仓库里有了3个产品。
仓库里有了4个产品。
仓库里有了5个产品。
仓库里有了6个产品。
仓库里有了7个产品。
仓库里有了8个产品。
仓库里有了9个产品。
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下8
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下7
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下6
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下5
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下4
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下3
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下2
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下1
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
仓库里有了2个产品。
仓库里有了3个产品。
仓库里有了4个产品。
仓库里有了5个产品。
仓库里有了6个产品。
仓库里有了7个产品。
仓库里有了8个产品。
仓库里有了9个产品。
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下8
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下7
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下6
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下5
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下4
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下3
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下2
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下1
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
仓库里有了2个产品。
仓库里有了3个产品。
仓库里有了4个产品。
仓库里有了5个产品。
仓库里有了6个产品。
仓库里有了7个产品。
仓库里有了8个产品。
仓库里有了9个产品。
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下8
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下7
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下6
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下5
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下4
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下3
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下2
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下1
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
仓库里有了2个产品。
仓库里有了3个产品。
仓库里有了4个产品。
仓库里有了5个产品。
仓库里有了6个产品。
仓库里有了7个产品。
仓库里有了8个产品。
仓库里有了9个产品。
仓库里有了10个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下9
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下8
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下7
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下6
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下5
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下4
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下3
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下2
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下1
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
仓库里有了2个产品。
仓库里有了3个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下2
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下1
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0
仓库里有了1个产品。
仓库里有了2个产品。
仓库里有了3个产品。
仓库里有了4个产品。
仓库里有了5个产品。
仓库里有了6个产品。
仓库里有了7个产品。
仓库里有了8个产品。
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下7
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下6
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下5
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下4
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下3
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下2
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下1
拿到了Fri Mar 06 11:14:49 CST 2020,现在仓库还剩下0

常见面试问题

用程序实现两个线程交替打印0~100的奇偶数

偶线程打印偶数

奇线程打印奇数

实现方式1

基本思路:synchronized

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
/**
* 描述:两个线程交替打印0~100的奇偶数,用synchronized关键字实现
*/
public class WaitNotifyPrintOddEvenSyn {

private static int count;

private static final Object lock = new Object();

//新建2个线程
//第一个只处理偶数,第二个只处理奇数(用位运算)
//用synchronized来通信
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();
}
}

在这其中两个线程相互之间抢夺资源,而不是你先执行我再执行这样平稳的运行

实现方式2

更好的方法: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
29
30
31
32
33
34
35
36
37
38
39
40
/**
* 描述:两个线程交替打印0~100的奇偶数,用wait和notify
*/
public class WaitNotifyPrintOddEveWait {

private static int count = 0;
private static final Object lock = new Object();


public static void main(String[] args) {
//先启动的线程并不一定先去执行,可以让主线程先进行休眠
new Thread(new TurningRunner(), "偶数").start();
Thread.sleep(100);
new Thread(new TurningRunner(), "奇数").start();
}

//1. 拿到锁,我们就打印
//2. 打印完,唤醒其他线程,自己就休眠
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()不需要

主要是为了让通信变得可靠,防止死锁与永久等待的发生,如果不把wait和notify都放在代码块中,很有可能在执行wait之前,线程突然切换到执行notify的线程,就改变了逻辑,容易造成死锁

而sleep主要是针对自己单独的线程,和其他线程没有什么关系

为什么线程通信的方法wait、notify和notifyAll被定义在Object类里?而sleep定义在Thread类里?

因为Java中的wait、notify和notifyAll属于锁机别的操作,而锁是属于某一个对象的,而并不是线程中

如果wait、notify和notifyAll定义在Thread中,就会造成很大的局限性,每一个线程确实可以唤醒等待,但当某个线程持有多个锁,并且这些锁之间是相互配合的。实际上某一个线程可以持有多把锁,如果把wait、notify和notifyAll定义在Thread类中,就没有办法实现这样灵活的逻辑了,所以Java所提供的锁是对于每一个对象都适用的

wait方法是属于Object对象的,那调用Thread.wait会怎么样?

1
2
Thread thread = new Thread();
thread.wait();

Thread也是一个对象,它也继承于Object类,可以把Thread作为锁去执行wait()/notify方法,但是对于Thread类它非常特殊,在线程退出的时候这个线程会自动的执行notify()方法,这样使我们的设计流程受到干扰,因为它自动的去执行notify()方法,所以这样就会使整个设计流程都会受到影响,所以Thread类不适合作为锁对象

如何选择用notify还是notifyAll?

选择notify的话,因为要唤醒的线程比较少,程序处理速度当然比notifyAll高出一些

但选择notify的时候,若这部分处理得不好,可能会有程序挂掉的危险性。一般来说,选择notifyAll所写出来的程序代码会比选择notify可靠。

除非你能确定程序员对程序代码的意义和能力限度一清二楚,否则选择notifyAll应该更稳妥一些

notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?

没有抢到锁的线程会等待,等待持有者去释放这把锁,然后再去抢

用suspend和resume来阻塞线程可以吗?为什么?

由于安全问题已经被弃用了,不推荐

suspend并不会破坏,但是suspend和resume不会释放锁,是带着锁进行休息的,这样容易造成死锁

1独占:因为suspend在调用过程中不会释放所占用的锁,所以如果使用不当会造成对公共对象的独占,使得其他线程无法访问公共对象,严重的话造成死锁

2不同步:容易出现因线程暂停导致的数据不同步

彩蛋:Java相关概念

JavaSE、JavaEE、JavaME是什么?

SE是标准版 EE是企业版 ME是移动版

而现在来说,只要是Java就是JavaSE

JRE和JDK和JVM是什么关系?

JRE包含JVM

JDK包含JRE

Java版本升级都包含了哪些东西的升级

不仅有代码的升级,还有JVM的升级,Java8的代码要去Java8的虚拟机上跑,两者打包一起组合成JRE

Java8和Java1.8和JDK8是什么关系,是同一个东西吗?

Java1.5就是Java5

JDK8是开发包,可理解为同一个东西

Java8和Java1.8和JDK8理解为同一个东西就可以了

sleep方法详解

作用

只想让线程在预期的时间执行,其他时间不要占用CPU资源

特点

不释放锁,无论是synchronized的monitor锁,还是普通的Lock都不释放,和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
25
26
/**
* 展示线程sleep的时候不释放synchronized的monitor,等sleep时间到了以后,正常结束后才释放锁
*/
public class SleepDontReleaseMonitor implements Runnable {

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

@Override
public void run() {
syn();
}

private synchronized void syn() {
System.out.println("线程" + Thread.currentThread().getName() + "获取到了monitor。");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "退出了同步代码块");
}
}

输出结果

1
2
3
4
线程Thread-0获取到了monitor。
线程Thread-0退出了同步代码块
线程Thread-1获取到了monitor。
线程Thread-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
/**
* 描述:演示sleep不释放lock(lock需要手动释放)
*/
public class SleepDontReleaseLock implements Runnable {

private static final Lock lock = new ReentrantLock();

@Override
public void run() {
lock.lock();
System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
try {
Thread.sleep(5000);
System.out.println("线程" + Thread.currentThread().getName() + "已经苏醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}

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

输出结果

1
2
3
4
线程Thread-0获取到了锁
线程Thread-0已经苏醒
线程Thread-1获取到了锁
线程Thread-1已经苏醒

sleep方法响应中断

第一步:抛出InterruptedException

第二步:清除中断状态

sleep的更优雅的写法

TimeUnit.SECONDS.sleep()

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
/**
* 描述:每个1秒钟输出当前时间,被中断,观察。
* Thread.sleep()
* TimeUnit.SECONDS.sleep()
*/
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.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(25);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("我被中断了!");
e.printStackTrace();
}
}
}
}

这种sleep中的内部代码,不会抛出异常

1
2
3
4
5
6
7
public void sleep(long timeout) throws InterruptedException {
if (timeout > 0) {
long ms = toMillis(timeout);
int ns = excessNanos(timeout, ms);
Thread.sleep(ms, ns);
}
}

普通的写法如果数值小于0则抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}

if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}

sleep(millis);
}

总结

sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常并清除中断状态

常见面试问题

wait/notify与sleep的异同

相同

  1. 都会让线程进入阻塞状态

  2. 可以响应中断

不同

  1. wait/notify必须在同步方法中执行,而sleep不需要
  2. wait/notify会释放锁,而sleep不释放锁
  3. wait/notify不必传递参数,则阻塞到直到被唤醒的时候,sleep必须有参数
  4. 所属的类不同

join方法

作用

因为新的线程加入我们,所以我们要等待它执行完成再出发

用法

main等待thread1执行完毕,注意谁等谁

三个例子

普通用法

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
/**
* 描述:演示join,注意语句输出顺序,会变化。
*/
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread thread = 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() + "执行完毕");
}
});

thread.start();
thread2.start();
System.out.println("开始等待子线程运行完毕");
thread.join();
thread2.join();
System.out.println("所有子线程执行完毕");
}
}

输出结果

1
2
3
4
开始等待子线程运行完毕
Thread-1执行完毕
Thread-0执行完毕
所有子线程执行完毕

在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
/**
* 描述: 演示join期间被中断的效果
*/
public class JoinInterrupt {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
mainThread.interrupt();
Thread.sleep(5000);
System.out.println("Thread1 finished.");
} catch (InterruptedException e) {
System.out.println("子线程中断");
}
}
});
thread1.start();
System.out.println("等待子线程运行完毕");
try {
thread1.join();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"主线程中断了");
//注意这里
thread1.interrupt();
}
System.out.println("子线程已运行完毕");
}

}

输出结果

1
2
3
4
等待子线程运行完毕
main主线程中断了
子线程已运行完毕
子线程中断

在一个线程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
24
25
26
27
/**
* 描述:先join再mainThread.getState()
* 通过debugger看线程join前后状态的对比
*/
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("子线程运行完毕");

}
}

输出结果

1
2
3
4
等待子线程运行完毕
WAITING
Thread-0运行结束
子线程运行完毕

join的注意点

CountDownLatch或CycliBarrier同样能达到join的效果

原理

1
2
3
public final void join() throws InterruptedException {
join(0);
}
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 final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}

实际上,每一个Thread类在run方法结束后会自动的执行notify的类似操作,不提倡Thread类的实例去执行wait,因为它会打乱我们自己的计划

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
/**
* 描述: 通过讲解join原理,分析出join的代替写法
*/
public class JoinPrinciple {

public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});

thread.start();
System.out.println("开始等待子线程运行完毕");
//thread.join();
//注意这里,等价替换
synchronized (thread) {
thread.wait();
}
System.out.println("所有子线程执行完毕");
}
}

输出结果

1
2
3
开始等待子线程运行完毕
Thread-0执行完毕
所有子线程执行完毕

面试题

在join期间,线程处于哪种线程状态

Waiting

yield方法

作用

释放当前线程CPU的时间片,此时这个线程的状态依然是Runnable,而不是Block,也不是Waiting,这是因为线程yield释放时间片,但不会释放锁,也不会陷入阻塞,下一次CPU调度依然会可能把这个线程调度起来

定位

JVM并不保证遵循yield的原则,一般开发中不使用yield

yield和sleep的区别

是否随时可能再次被调度

线程sleep期间,线程调度的时候认为这个线程已经被阻塞了,不会在调度这个线程

线程yield期间,它只是暂时的把资源让给别的线程,但是可以立刻处于竞争者状态,可以随时被调度

线程的各个属性

什么时候需要设置守护线程?

应该如何应用线程优先级来帮助程序运行?有哪些禁忌?

不同的操作系统如何处理优先级问题?

线程各属性纵览

属性名称 用途
编号(ID) 每个线程有自己的ID,用于标识不同的线程,给系统使用的
名字(Name) 在开发、调试或运行的过程中,更容易区分每个不同的线程、定位问题等,给用户使用的
是否是守护线程(isDaemon) true代表该线程是守护线程,false代表线程是非守护线程,也就是用户线程
优先级(Priority) 优先级这个属性的目的是告诉线程调度器,用户希望哪些线程相对的多运行,哪些少运行

线程ID

每个线程有自己的ID,并且ID是不能修改的,线程的ID会从1开始不停的往上自加,mian函数就是第一个线程

1
2
3
4
5
6
7
8
9
10
11
/**
* 描述:ID从1开始,JVM运行起来后,我们自己创建的线程的ID早已不是2.
*/
public class Id {

public static void main(String[] args) {
Thread thread = new Thread();
System.out.println("主线程的ID"+Thread.currentThread().getId());
System.out.println("子线程的ID"+thread.getId());
}
}

输出结果

1
2
主线程的ID1
子线程的ID11

在Thread类的init方法中

1
2
/* Set thread ID */
tid = nextThreadID();

而nextThreadID方法中

1
2
3
4
5
6
7
8
9
10
11
/*
* Thread ID
*/
private long tid;

/* For generating thread ID */
private static long threadSeqNumber;

private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}

由nextThreadID()方法可以看出,第一个线程是从1开始而不是0

而在运行程序的时候,JVM同时帮助创建多个线程,所以子线程会从11开始

线程名字

默认线程名字源码分析

1
2
3
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

对于没有指定名字的线程,在初始化的时候会传入一个默认的名字

nextThreadNum方法

1
2
3
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}

因为方法使用synchronized来修饰,所以就不会产生重名的情况

修改线程的名字

1
2
3
4
5
6
7
8
9
10
11
public final synchronized void setName(String name) {
checkAccess();
if (name == null) {
throw new NullPointerException("name cannot be null");
}

this.name = name;
if (threadStatus != 0) {
setNativeName(name);
}
}

如果开始的时候忘记设置线程的名字,则依然可以使用setName()来设置线程的名字

守护线程

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

垃圾处理线程就是守护线程

守护线程的三个特性

1.线程类型默认继承自父线程

我们自己创建的线程还是用户线程,父线程就是用户线程

用户线程创建的线程还是用户线程,守护线程创建的线程还是守护线程

如果用用户线程去创建守护线程,则需要认为的修改属性,使用setDaemon来进行修改

2.通常而言所有的守护线程是由JVM自动启动的,而不是由用户启动的,在JVM启动的时候会有一个非守护线程,就是main函数

3.不影响JVM退出

对于JVM而言,它想退出的时候只看有没有用户线程,不看有没有守护线程

线程优先级

线程一共10个优先级,默认为5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 /**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;

/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;

/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;

程序设计不应该依赖于优先级

因为不同操作系统不一样,并且优先级会被操作系统改变

在开发过程中,没有必要对线程的优先级进行修改

各属性总结

属性名称 用途 注意事项
编号(ID) 标识不同的线程 被后续创建的线程使用,唯一性,不允许被修改,系统使用
名字(Name) 定位问题 清晰有意义的名字,默认的名字,用户使用
是否是守护线程(isDaemon) 守护线程、用户线程 二选一,默认继承父线程,想修改使用setDaemon但没有必要
优先级(Priority) 相对多运行 默认和父线程的优先级相等,共有10个等级,默认值是5,不应依赖优先级去做业务逻辑

面试常见问题

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

1整体无区别,用户线程是处理逻辑的,而守护线程是给用户线程提供服务的

2唯一区别在于JVM的离开

如果是用户线程则会影响到JVM是否退出,守护线程不会影响JVM是否退出

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

不应该把线程设置为守护线程,设置为守护线程会非常危险,可能会让JVM强制终止,开发过程中不需要使用守护线程,因为JVM自身为用户提供的守护线程足够服务于用户

什么时候我们需要设置守护线程

通常情况下,我们不需要设置守护线程,JVM所提供的守护线程足够使用

未捕获异常UncaughtException如何处理

考考你

Java异常体系图

实际工作中,如何全局处理异常?为什么要全局处理?不处理行不行?

为什么需要UncaughtExceptionHandler

1主线程可以轻松发现异常,子线程却不行

单线程的时候抛出异常,并进行处理,打印出异常堆栈,但是在多线程的情况下,子线程如果抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 子线程发生异常,会有什么不同?
*/
public class ExceptionInChildThread implements Runnable {

public static void main(String[] args) {
new Thread(new ExceptionInChildThread()).start();
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}

@Override
public void run() {
throw new RuntimeException();
}
}

输出结果

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126Exception in thread "Thread-0"
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
java.lang.RuntimeException
158 at threadcoreknowledge.uncaughtexception.ExceptionInChildThread.run(ExceptionInChildThread.java:17)
at java.lang.Thread.run(Thread.java:748)

159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199

子线程会抛出异常,但是对主线程丝毫没有影响,主线程依然会执行

2子线程异常无法用传统方法捕获

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
/**
* 描述:1. 不加try/catch抛出4个异常,都带线程名字
*
* 说明线程的异常不能用传统方法捕获
*/
public class CantCatchDirectly implements Runnable {

public static void main(String[] args) throws InterruptedException {

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

}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
Exception in thread "MyThread-1" java.lang.RuntimeException
at threadcoreknowledge.uncaughtexception.CantCatchDirectly.run(CantCatchDirectly.java:26)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "MyThread-2" java.lang.RuntimeException
at threadcoreknowledge.uncaughtexception.CantCatchDirectly.run(CantCatchDirectly.java:26)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "MyThread-3" java.lang.RuntimeException
at threadcoreknowledge.uncaughtexception.CantCatchDirectly.run(CantCatchDirectly.java:26)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "MyThread-4" java.lang.RuntimeException
at threadcoreknowledge.uncaughtexception.CantCatchDirectly.run(CantCatchDirectly.java:26)
at java.lang.Thread.run(Thread.java:748)

如上结果可以看出,不加try/catch抛出4个异常,都带线程名字

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 CantCatchDirectly implements Runnable {

public static void main(String[] args) throws InterruptedException {
try {
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();
} catch (RuntimeException e) {
System.out.println("Caught Exception.");
}

}

@Override
public void run() {

throw new RuntimeException();

}
}

输出结果

1
2
3
4
5
6
7
8
9
10
11
12
Exception in thread "MyThread-1" java.lang.RuntimeException
at threadcoreknowledge.uncaughtexception.CantCatchDirectly.run(CantCatchDirectly.java:30)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "MyThread-2" java.lang.RuntimeException
at threadcoreknowledge.uncaughtexception.CantCatchDirectly.run(CantCatchDirectly.java:30)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "MyThread-3" java.lang.RuntimeException
at threadcoreknowledge.uncaughtexception.CantCatchDirectly.run(CantCatchDirectly.java:30)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "MyThread-4" java.lang.RuntimeException
at threadcoreknowledge.uncaughtexception.CantCatchDirectly.run(CantCatchDirectly.java:30)
at java.lang.Thread.run(Thread.java:748)

加了try/catch,期望捕获到第一个线程的异常,线程234不应该运行,希望看到打印出CaughtException,执行时发现,根本没有CaughtException,线程234依然运行并且抛出异常

try-catch只能捕获对应线程类的异常,但不能捕获到子线程的异常

3使用UncaughtExceptionHandler可以提高程序的健壮性

如果不能直接捕获,子线程终止,但是不能处理子线程的异常

两种解决办法

方案一(不推荐)

手动在每个run方法里进行try-catch

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 CantCatchDirectly implements Runnable {

public static void main(String[] args) throws InterruptedException {

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() {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
System.out.println("Caught Exception.");
}
}
}

输出结果

1
2
3
4
Caught Exception.
Caught Exception.
Caught Exception.
Caught Exception.

方案二(推荐)

利用UncaughtExceptionHandler来处理异常,它是Thread类所提供的处理器,他能够检查出线程由于未捕获异常而终止的情况,并且对此进行处理

UncaughtExceptionHandler接口

1
2
3
4
5
6
7
8
9
10
11
12
13
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
//哪个线程抛出哪个异常
void uncaughtException(Thread t, Throwable e);
}

异常处理器调用策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ThreadGroup implements Thread.UncaughtExceptionHandler { 
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);
}
}
}
}

如果没有实现DefaultUncaughtExceptionHandler,则此对象为空,如果不为空,则可以对该线程的异常进行处理

其中setDefaultUncaughtExceptionHandler方法

1
2
3
4
5
6
7
8
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setDefaultUncaughtExceptionHandler"));
}

defaultUncaughtExceptionHandler = eh;
}

实现方式

三种实现方式

1.给程序统一设置(推荐)

2.给每个线程单独设置

3.给线程池设置

给每个线程和给线程池设置相当于精细化处理,开发中使用并不多

自己设置MyUncaughtExceptionHanlder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 描述:自己的MyUncaughtExceptionHanlder
*/
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
17
18
19
20
21
22
23
/**
* 描述:使用刚才自己写的UncaughtExceptionHandler
*/
public class UseOwnUncaughtExceptionHandler implements Runnable {

public static void main(String[] args) throws InterruptedException {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));

new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-1").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-2").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-3").start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-4").start();
}


@Override
public void run() {
throw new RuntimeException();
}
}

输出结果

image-20200307131526595

面试题

Java异常体系

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

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

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

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

image-20200202005758106

多线程的弊端主要分为两大部分:线程安全与性能问题

考一考

一共有哪几种线程安全问题?3种

哪些场景需要额外注意线程安全问题?

什么是多线程带来的上下文切换?

线程安全

什么是线程安全

<<Java Concurrency In Practice>>的作者Brian Goetz对“线程安全”有一个比较恰当的定义:

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协作操作,调用这个对象的行为都可以获得正确的结果,那这个对象时线程安全的

翻译:

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

线程不安全

不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据

如果一个类完全不用于多线程,就没有必要设计成线程安全的

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

第一种情况就是数据争用,最典型的案例就是两个线程同时去写,同时写就会导致其中一个线程中的数据要么被丢弃,要么写入错误,最终造成错误数据

第二种情况就是竞争条件,竞争条件主要指的是执行顺序,比如想要读取一个文件的内容,自然应该是写入文件之后的,但如果线程之间配合不好,在一个线程没有写完另一个线程就去读取,这就会造成顺序上的错误

案例1运行结果错误

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
/**
* 描述:第一种:运行结果出错。
* 演示计数不准确(减少),找出具体出错的位置。
*/
public class MultiThreadsError implements Runnable {

int index=0;

static MultiThreadsError instance=new MultiThreadsError();
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);
}


@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
}
}
}

输出结果

1
19485

如图

image-20200307192244172

现在知道确实得到的数据要比20000要少,但是哪几次少,少在哪里,就像得出的结果是19485,则少了515次,现在想知道少的这515次是在哪一次少的,当这些少的次数一旦发生的时候,希望能够得到发生的问题的位置

升级1

既然两个线程会出现线程的争抢,出现数据的错误,则在每一个线程执行index++之后,就立刻对线程进行检查

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 MultiThreadsError implements Runnable {

int index = 0;

//真正所加的数量
static AtomicInteger realIndex = new AtomicInteger();
//出错的数量
static AtomicInteger wrongCount = new AtomicInteger();

final boolean[] marked = new boolean[10000000];

static MultiThreadsError instance = new MultiThreadsError();

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


@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
//正确的数量增加
realIndex.incrementAndGet();
//第一个线程进来执行index++,并将index设置为true,第二个线程进来在执行index++之后检查marked[index]是否为true,如果为true则说明使用的是第一个线程所使用过的数值,则这个数值发生了错误,所以先检查是否marked[index]被标记过
if (marked[index]) {
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
发生了错误241
发生了错误1535
发生了错误2118
发生了错误2118
发生了错误3782
发生了错误4543
发生了错误5872
发生了错误7100
发生了错误8236
发生了错误9480
发生了错误10694
发生了错误11762
发生了错误12919
表面上结果是19976
真正运行的次数20000
错误运行的次数13

此时得到的结果发现表面上结果是中的值,与错误运行的次数中的值相加之后得到的结果不为2000,可能发生的原因是当第一个线程执行realIndex.incrementAndGet();语句,之后执行if (marked[index]) {,,因为为false所以没有进入其中执行,当要执行marked[index] = true;的时候,此时第二个线程抢夺了资源,执行到if (marked[index]) {的时候也是false,没有进入执行,所以会出现表面上结果是与错误运行的次数相加结果少于20000的情况

升级2

添加闸门

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

int index = 0;

//真正所加的数量
static AtomicInteger realIndex = new AtomicInteger();
//出错的数量
static AtomicInteger wrongCount = new AtomicInteger();

static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);

final boolean[] marked = new boolean[10000000];

static MultiThreadsError instance = new MultiThreadsError();

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


@Override
public void run() {
marked[0]=true;
for (int i = 0; i < 10000; i++) {
try {
//如果有两个线程都执行过await(),代表所等待的内容都到齐了,就会放行
cyclicBarrier1.await();

} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
index++;

//正确的数量增加
realIndex.incrementAndGet();
synchronized (instance) {
//第一个线程进来执行index++,并将index设置为true,第二个线程进来在执行index++之后检查marked[index]是否为true,如果为true则说明使用的是第一个线程所使用过的数值,则这个数值发生了错误,所以先检查是否marked[index]被标记过
//要求前一个位置的marked[index-1]也为true
if (marked[index]&&marked[index-1]) {
System.out.println("发生了错误" + index);
//错误的数量进行增加
wrongCount.incrementAndGet();
}
marked[index] = true;
}

}
}

}

输出结果

1
2
3
4
5
发生了错误13071
发生了错误19712
表面上结果是19998
真正运行的次数20000
错误运行的次数2

案例2死锁、活锁、饥饿

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
/**
* 描述:演示死锁。
*/
public class MultiThreadError implements Runnable {

int flag ;
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 {
System.out.println(Thread.currentThread().getName());
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
}

输出结果

1
2
3
4
flag = 1
flag = 0
Thread-0
Thread-1

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

什么是对象发布

使对象能够被当前范围之外的代码所看见。比如通过类的非私有方法返回对象的引用

什么是逸出

一种错误的发布。当一个对象还没有构造完成时,就被其他线程所看见

案例1

方法返回一个private对象,private的本意是不让外部访问对象

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

private Map<String, String> states;

public MultiThreadsError3() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}

public Map<String, String> getStates() {
return states;
}


public static void main(String[] args) {
MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
Map<String, String> states = multiThreadsError3.getStates();
System.out.println(states.get("1"));
//注意这里可以将声明为private的states删除掉
states.remove("1");
System.out.println(states.get("1"));

}
}

输出结果

1
2
周一
null

由于外部的操作造成这样的原因就是因为通过构造函数把本应该私有的变量states给发布出去了

1
2
3
public Map<String, String> getStates() {
return states;
}

案例2

还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,如下

1在构造函数中未初始化完毕就this赋值

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
/**
* 描述:初始化未完毕,就this赋值
*/
public class MultiThreadsError4 {

static Point point;

public static void main(String[] args) throws InterruptedException {
new PointMaker().start();
//注意这里
Thread.sleep(10);
if (point != null) {
System.out.println(point);
}
}
}

class Point {

private final int x, y;

public Point(int x, int y) throws InterruptedException {
this.x = x;
MultiThreadsError4.point = this;
Thread.sleep(100);
this.y = y;
}

@Override
public String toString() {
return x + "," + y;
}
}

class PointMaker extends Thread {

@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出结果

1
1,0
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
public class MultiThreadsError4 {

static Point point;

public static void main(String[] args) throws InterruptedException {
new PointMaker().start();
Thread.sleep(105);
if (point != null) {
System.out.println(point);
}
}
}

class Point {

private final int x, y;

public Point(int x, int y) throws InterruptedException {
this.x = x;
MultiThreadsError4.point = this;
Thread.sleep(100);
this.y = y;
}

@Override
public String toString() {
return x + "," + y;
}
}

class PointMaker extends Thread {

@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

输出结果

1
1,1

出现以上的原因是因为对y的初始化是比较迟的,对x初始化是比较早的,x的值与y的值有先有后,由于过早的把

MultiThreadsError4.point = this;发布出来了,所以导致看到的x与y在不同的时刻是不一样的

2隐式逸出,注册监听事件

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
/**
* 描述: 观察者模式
*/
public class MultiThreadsError5 {

int count;

public MultiThreadsError5(MySource source) {
source.registerListener(new EventListener() {
@Override
public void onEvent(Event e) {
System.out.println("\n我得到的数字是" + count);
}

});
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}

public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Event() {
});
}
}).start();
MultiThreadsError5 multiThreadsError5 = new MultiThreadsError5(mySource);
}

static class MySource {

private EventListener listener;

void registerListener(EventListener eventListener) {
this.listener = eventListener;
}

void eventCome(Event e) {
if (listener != null) {
listener.onEvent(e);
} else {
System.out.println("还未初始化完毕");
}
}

}

interface EventListener {

void onEvent(Event e);
}

interface Event {

}
}

3构造函数中运行线程

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
/**
* 描述:构造函数中新建线程
*/
public class MultiThreadsError6 {

private Map<String, String> states;

public MultiThreadsError6() {
//在构造函数中开启了一个线程
new Thread(new Runnable() {
@Override
public void run() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
}).start();
}

public Map<String, String> getStates() {
return states;
}

public static void main(String[] args) throws InterruptedException {
MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();
System.out.println(multiThreadsError6.getStates().get("1"));
}
}

输出结果

1
2
Exception in thread "main" java.lang.NullPointerException
at background.MultiThreadsError6.main

构造函数中不应该以新建线程的方式做初始化,因为初始化的工作在另外一个线程中还没有初始化完毕,所以会报空指针异常

如果有一定时间的等待,则会取到正确的值

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 MultiThreadsError6 {

private Map<String, String> states;

public MultiThreadsError6() {
//在构造函数中开启了一个线程
new Thread(new Runnable() {
@Override
public void run() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
}).start();
}

public Map<String, String> getStates() {
return states;
}

public static void main(String[] args) throws InterruptedException {
MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();
Thread.sleep(1000);
System.out.println(multiThreadsError6.getStates().get("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
public class MultiThreadsError3 {

private Map<String, String> states;

public MultiThreadsError3() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}

public Map<String, String> getStates() {
return states;
}

public Map<String,String> getStatesImproved(){
return new HashMap<>(states);
}


public static void main(String[] args) {
MultiThreadsError3 multiThreadsError3 = new MultiThreadsError3();
System.out.println(multiThreadsError3.getStatesImproved().get("1"));
multiThreadsError3.getStatesImproved().remove("1");
System.out.println(multiThreadsError3.getStatesImproved().get("1"));
}
}

输出结果

1
2
周一
周一

针对第二种可以采用工厂模式的方法

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
71
72
/**
* 描述:用工厂模式修复刚才的初始化问题
*/
public class MultiThreadsError7 {

int count;
private EventListener listener;

private MultiThreadsError7(MySource source) {
listener = new EventListener() {
@Override
public void onEvent(MultiThreadsError5.Event e) {
System.out.println("\n我得到的数字是" + count);
}

};
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}

public static MultiThreadsError7 getInstance(MySource source) {
MultiThreadsError7 safeListener = new MultiThreadsError7(source);
source.registerListener(safeListener.listener);
return safeListener;
}

public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new MultiThreadsError5.Event() {
});
}
}).start();
MultiThreadsError7 multiThreadsError7 = new MultiThreadsError7(mySource);
}

static class MySource {

private EventListener listener;

void registerListener(EventListener eventListener) {
this.listener = eventListener;
}

void eventCome(MultiThreadsError5.Event e) {
if (listener != null) {
listener.onEvent(e);
} else {
System.out.println("还未初始化完毕");
}
}

}

interface EventListener {

void onEvent(MultiThreadsError5.Event e);
}

interface Event {

}
}

总结

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

  • 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等

  • 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题,主要有两种,一个是先读取再修改,一个是先检查再执行

  • 不同的数据之间存在绑定关系的时候,一个非常典型的例子就是IP和端口号,它们之间不能说是只修改一个不修改另一个

  • 使用其他类的时候,如果对方没有声明自己是线程安全的会出现线程安全的情况,所以应该考虑,如果是线程安全的类,应该标记为安全的,如果是线程不安全的类,也应该标记为不安全的

多线程带来的性能问题

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

第一个是调度原因,上下文切换

第二个是协作原因,内存同步

调度

什么是上下文切换

上下文

首先,需要讲清楚什么是上下文。

每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,这就涉及到 CPU 寄存器 和 程序计数器(PC):

  • CPU 寄存器是 CPU 内置的容量小、但速度极快的内存
  • 程序计数器会存储 CPU 正在执行的指令位置,或者即将执行的指令位置

这两个是 CPU 运行任何任务前都必须依赖的环境,因此叫做 CPU 上下文。

上下文切换

那么,什么是上下文切换呢?下面是一个上下文切换时需要履行的步骤:

  1. 将前一个 CPU 的上下文(也就是 CPU 寄存器和程序计数器里边的内容)保存起来

  2. 然后加载新任务的上下文到寄存器和程序计数器

  3. 最后跳转到程序计数器所指的新位置,运行新任务

被保存起来的上下文会存储到系统内核中,等待任务重新调度执行时再次加载进来。

CPU 的上下文切换分三种:进程上下文切换、线程上下文切换、中断上下文切换。

何谓缓存失效

对于一个并发量大的项目,缓存是必须的,如果没有缓存,所有的请求将直击数据库,数据库很有可能抗不住,所以建立缓存势在不行。

那么建立缓存后就有可能出现缓存失效的问题:

  1. 大面积的缓存key失效
  2. 热点key失效

类似12306网站,因为用户频繁的查询车次信息,假设所有车次信息都建立对应的缓存,那么如果所有车次建立缓存的时间一样,失效时间也一样,那么在缓存失效的这一刻,也就意味着所有车次的缓存都失效。通常当缓存失效的时候我们需要重构缓存,这时所有的车次都将面临重构缓存,即出现问题1的场景,此时数据库就将面临大规模的访问。
针对以上这种情况,可以将建立缓存的时间进行分布,使得缓存失效时间尽量不同,从而避免大面积的缓存失效。

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

抢锁、IO

常见面试问题

有多少种实现线程的方法?思路有5点

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

一个线程两次调用start()方法会调用run()方法,为什么选择调用start()方法,而不是直接调用run()方法?

如何停止线程?

如何处理不可中断的阻塞?

用程序实现两个线程交替打印0~100的奇偶数

手写生产者消费者模式

为什么wait()需要在同步代码块中使用,而sleep()不需要

为什么线程通信的方法wait(),notify()和notifyAll()被定义在Object类里,而sleep定义在Thread类里?

wait()方法属于Object对象的,那调用Thread.wait()会怎么样?

如何选择用notify还是notifyAll?

notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败会怎么样?

用suspend()和resume()来阻塞线程可以吗?为什么?

wait()/notify()、sleep()异同

在join期间,线程处于哪种线程状态

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

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

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

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

什么是多线程的上下文切换

文章目录
  1. 1. 线程8大核心基础
    1. 1.1. 实现多线程的方法到底有1种还是2种还是4种
      1. 1.1.1. 网上说法
      2. 1.1.2. 正确说法
        1. 1.1.2.1. 方法一
        2. 1.1.2.2. 方法二
      3. 1.1.3. 两种方法的对比
      4. 1.1.4. 两种方法的本质区别
      5. 1.1.5. 思考题
      6. 1.1.6. 总结
      7. 1.1.7. 经典错误观点
      8. 1.1.8. 彩蛋
        1. 1.1.8.1. 如何从宏观和微观两个方面来提高技术
        2. 1.1.8.2. 如何了解技术领域的前沿动态
      9. 1.1.9. 常见面试题
    2. 1.2. 启动线程的正确和错误方式
      1. 1.2.1. start()和run()的比较
      2. 1.2.2. start()方法原理解读
        1. 1.2.2.1. start()方法含义12
          1. 1.2.2.1.1. 1启动新线程
          2. 1.2.2.1.2. 2start()方法的准备工作
          3. 1.2.2.1.3. 不能重复执行start()方法
        2. 1.2.2.2. 源码解析
      3. 1.2.3. run()方法原理解读
      4. 1.2.4. 常见面试题
    3. 1.3. 如何正确停止线程
      1. 1.3.1. 原理介绍
      2. 1.3.2. 最佳实践:如何正确停止线程
        1. 1.3.2.1. 通常线程会在什么情况下停止
        2. 1.3.2.2. 使用interrupt的几种情况
          1. 1.3.2.2.1. 1正常情况下停止线程
          2. 1.3.2.2.2. 2线程被阻塞的时候停止线程
          3. 1.3.2.2.3. 3如果线程在每次迭代的时候都阻塞
          4. 1.3.2.2.4. 4while内try/catch的问题
        3. 1.3.2.3. 实际开发中的两种最佳实践
      3. 1.3.3. 响应中断的方法总结列表
      4. 1.3.4. 正确停止带来的好处
      5. 1.3.5. 错误的停止方法
        1. 1.3.5.1. 1被弃用的stop方法
        2. 1.3.5.2. 2suspend和resume方法
        3. 1.3.5.3. 3用volatile设置boolean标记位
          1. 1.3.5.3.1. 看上去可行
          2. 1.3.5.3.2. 错误之处
          3. 1.3.5.3.3. 修正方案
      6. 1.3.6. 停止线程相关的重要函数解析
      7. 1.3.7. 常见面试题
        1. 1.3.7.1. 1如何停止线程
        2. 1.3.7.2. 2如何处理不可中断的阻塞
    4. 1.4. 线程的6个状态(生命周期)
      1. 1.4.1. 线程的6种状态
      2. 1.4.2. 每个状态的含义
        1. 1.4.2.1. New
        2. 1.4.2.2. Runnable
        3. 1.4.2.3. Blocked
        4. 1.4.2.4. Waiting
        5. 1.4.2.5. Timed Waiting
        6. 1.4.2.6. Terminated
      3. 1.4.3. 状态间的转化图示
      4. 1.4.4. 状态转化的特殊情况
      5. 1.4.5. 阻塞状态是什么
      6. 1.4.6. 常见面试问题
    5. 1.5. Thread类和Object类中的重要方法解析
      1. 1.5.1. 方法概览
    6. 1.6. wait、notify、notifyAll方法详解
      1. 1.6.1. 作用与用法
      2. 1.6.2. 代码演示
        1. 1.6.2.1. 1普通用法
        2. 1.6.2.2. 2notify和notifyAll的用法
        3. 1.6.2.3. 3只释放当前monitor的用法
      3. 1.6.3. wait、notify、notifyAll特点
      4. 1.6.4. wait原理
      5. 1.6.5. 手写生产者消费者设计模式
        1. 1.6.5.1. 为什么要使用生产者和消费者模式
      6. 1.6.6. 常见面试问题
        1. 1.6.6.1. 用程序实现两个线程交替打印0~100的奇偶数
        2. 1.6.6.2. 手写生产者消费者设计模式
        3. 1.6.6.3. 为什么wait()需要在同步代码块内使用,而sleep()不需要
        4. 1.6.6.4. 为什么线程通信的方法wait、notify和notifyAll被定义在Object类里?而sleep定义在Thread类里?
        5. 1.6.6.5. wait方法是属于Object对象的,那调用Thread.wait会怎么样?
        6. 1.6.6.6. 如何选择用notify还是notifyAll?
        7. 1.6.6.7. notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
        8. 1.6.6.8. 用suspend和resume来阻塞线程可以吗?为什么?
    7. 1.7. 彩蛋:Java相关概念
    8. 1.8. sleep方法详解
      1. 1.8.1. 作用
      2. 1.8.2. 特点
      3. 1.8.3. sleep方法响应中断
      4. 1.8.4. sleep的更优雅的写法
      5. 1.8.5. 总结
      6. 1.8.6. 常见面试问题
    9. 1.9. join方法
      1. 1.9.1. 作用
      2. 1.9.2. 用法
      3. 1.9.3. 三个例子
        1. 1.9.3.1. 普通用法
        2. 1.9.3.2. 在join遇到中断的时候
        3. 1.9.3.3. 在一个线程join期间,另一个线程是什么状态
      4. 1.9.4. join的注意点
      5. 1.9.5. 原理
      6. 1.9.6. join的等价代码
      7. 1.9.7. 面试题
    10. 1.10. yield方法
      1. 1.10.1. 作用
      2. 1.10.2. 定位
      3. 1.10.3. yield和sleep的区别
    11. 1.11. 线程的各个属性
      1. 1.11.1. 线程各属性纵览
      2. 1.11.2. 线程ID
      3. 1.11.3. 线程名字
        1. 1.11.3.1. 默认线程名字源码分析
        2. 1.11.3.2. 修改线程的名字
      4. 1.11.4. 守护线程
        1. 1.11.4.1. 守护线程的三个特性
      5. 1.11.5. 线程优先级
      6. 1.11.6. 各属性总结
      7. 1.11.7. 面试常见问题
    12. 1.12. 未捕获异常UncaughtException如何处理
      1. 1.12.1. 为什么需要UncaughtExceptionHandler
        1. 1.12.1.1. 1主线程可以轻松发现异常,子线程却不行
        2. 1.12.1.2. 2子线程异常无法用传统方法捕获
        3. 1.12.1.3. 3使用UncaughtExceptionHandler可以提高程序的健壮性
      2. 1.12.2. 两种解决办法
        1. 1.12.2.1. 方案一(不推荐)
        2. 1.12.2.2. 方案二(推荐)
      3. 1.12.3. 实现方式
      4. 1.12.4. 面试题
    13. 1.13. 双刃剑:多线程会导致的问题
      1. 1.13.1. 线程安全
        1. 1.13.1.1. 什么是线程安全
        2. 1.13.1.2. 线程不安全
    14. 1.14. 什么情况下会出现线程安全问题,如何避免
      1. 1.14.1. 案例1运行结果错误
      2. 1.14.2. 案例2死锁、活锁、饥饿
      3. 1.14.3. 案例3对象发布和初始化的时候的安全问题
        1. 1.14.3.1. 什么是对象发布
        2. 1.14.3.2. 什么是逸出
        3. 1.14.3.3. 案例1
        4. 1.14.3.4. 案例2
        5. 1.14.3.5. 如何解决逸出
      4. 1.14.4. 总结
    15. 1.15. 多线程带来的性能问题
      1. 1.15.1. 为什么多线程会带来性能问题
      2. 1.15.2. 调度
        1. 1.15.2.1. 什么是上下文切换
        2. 1.15.2.2. 何谓缓存失效
        3. 1.15.2.3. 何时会导致密集的上下文切换
    16. 1.16. 常见面试问题
|