Java并发核心知识体系精讲——线程8大核心基础(一)
实现多线程的方法到底是1种还是2种还是4种?
网上的说法
网上说法很多,有两种、三种、四种,每一种说法看起来都很有说服力,都是从不同的角度进行分析。
正确的说法
Oracle官网的文档是如何写的?
翻译成中文后如下:
如图中描述,第一种是声明成Thread类的子类,第二种方法是实现Runnable接口。
oracle官网文档
方法一:实现Runnable接口
1 | public class RunnableStyle implements Runnable{ |
方法二:继承Thread类
1 | public class ThreadStyle extends Thread{ |
两种方法对比
方法一(实现Runnable接口更好)
- 首先从架构角度,线程执行的任务,也就是run中运行的具体代码,应该是和Thread类是解耦的;
- 使用继承Thread类的方法,无法使用线程池;
- 由于java不支持多继承,所以继承了Thread之后就无法再继承其他类,不便于扩展。
两种方法的本质对比
方法一:最总调用target.run()
target
就是通过Thread
有参构造传入的Runnable
接口的实现类,所以再Thread
中最终还是会调用Runnable
的实现类重写的run()
方法
1 |
|
方法二:run()
整个都被重写
run()
方法整个都被重写之后,Thread
最终就会直接调用被重写后run()
方法
如果同时使用两种方法会怎么样?
代码如下:
1 | public class BothRunnableThread { |
最终打印结果是我来自Thread
,因为在Thread
类中,是先调用Thread
类的run()
方法,然后target.run()
再调用作为参数的Runnable接口的实现的run()
方法,
而代码中直接重写了Thread
的run()
方法,就不会再调用target.run()
总结:最精准的描述
- 通常我们可以分为两类,Oracle也是这么说的;
- 准确的讲,创建线程只有一种方法那就是构建Thread类,而实现线程的执行单元有两种方式。
方法一:实现Runnable接口的run方法,并把Runnable实例传给Thread类;
方法二:重写Thread的run方法(继承Thread类)
经典的错误观点
线程池创建线程也算是一种新建线程的方法
对线程池的创建,最终都会调用到如图所示的方法,其中使用了Executors.defaultThreadFactory()
,这个工程类就是创建线程池用的
最终在默认情况下,使用的DefaultThreadFactory
类,如图中所示,最终还是通过Thread
类来创建的线程
通过Callable
和FutureTask
创建线程,也算是一种新建线程的方式
Callable
主要是依赖于FutureTask
,而FutureTask是继承于Runnable
,最后依然还是需要依靠Thread
来进行创建线程
无返回值是实现Runnable接口,有返回值是实现Callable接口,所以Callable是新的实现线程的方式
此观点和上面的观点是一样的,Callable
接口获取返回值依靠的是FutureTask
,而FutureTask
依然是对Runnable
的实现
定时器创建线程
代码如下,创建了一个每隔一秒打印线程名称的定时器,定时器的逻辑部分主要是依靠抽象类TimerTask的run()方法来实现的,而TimerTask也是继承与Runnable接口,Timer类内部依然是通过Thread来运行的线程
1 | public class DemoTimerTask { |
匿名内部类创建线程
具体代码如下,直接通过匿名内部类重写Thread
或者实现Runnable
的run()
方法,实际上和通过类继承Thread
类或者实现Runnable
接口去重写run()
没有什么区别
1 | public class AnonymousInnerClassDemo { |
典型错误观点总结
- 多线程的实现方式,在代码中写法千变万化,但其本质万变不离其宗,都是通过之前所说的那两种方法实现的(继承
Thread
重写run()
方法或实现Runnable
接口实现run()
方法)
Lambda表达式创建线程
代码如下,实际是通过函数式编程实现的,其本质和实现Runnable
的匿名内部类没有区别
1 | public class Lambda { |
如何从宏观和微观两个方面来提升技术?如何了解技术领域的前沿动态?如何在业务开发中成长?
宏观上
- 并不是靠工作年限,有的人工作了5年技术却还只是懂皮毛。
- 要有强大的责任心,不放过任何bug,找到原因并去解决,这就是提高。
- 主动:永远不会觉得自己的时间多余,重构、优化、学习、总结等。
- 敢于承担:虽然这个技术难题以前没有碰到过,但是在一定的了解调研后,敢于承担技术难题,让工作充满挑战,这一次次攻克难关的过程中,进步是飞速的。
- 关系产品,关系业务,而不只是写的代码。
微观上
- 看经典书籍(指外国人写的经典的中国译本,比如说Java并发编程实战、自顶向下计算机网络)
- 看官方文档
- 英文搜google和stackoverflow
- 自己动手,实践写demo,尝试用到项目里面
- 不理解的参考该领域的多个书本,综合判断
- 学习开源项目,分析源码(学习synchronized原理,反编译看cpp代码)
常见面试问题
有多少中实现线程大的方法?
- 从不同的角度看,会有不同的答案
- 典型答案是两种
- 我们看原理,两种本质都是一样的
- 具体展开说其他的方式
- 结论
实现Runnable
接口和继承Thread
类那种方式更好
- 从代码架构角度(可以将线程分为两个部分,一是线程运行的具体任务,也就是实现run方法中的内容,二是线程的生命周期,也就是创建线程启动线程运行线程结束线程,这部分是
Thread
类实现的) - 新建线程大的损耗(继承
Thread
类的话,每次我们想要运行任务都需要创建一个线程,而实现Runnable
接口的话,可以使用线程池,每次都使用同一个线程运行)【继承Thread
类应该也是可以使用线程池的,只要不调用start
方法去创建线程,线程池需要的是一个Runnable
的实现类,而Thread
本身也是Runnable
接口大的实现类】 - Java不支持双继承
怎样才是正确的线程启动方式?
start()
和run()
的比较
代码如下:
1 | public class StartAndRunMethhodd { |
结果:
1 | main |
start()
方法原理解读
start()
方法的含义
启动新线程
当主线程在调用了
start
方法之后,会告诉jvm需要启动一个新大的线程,至于线程何时能够启动,是由线程调度器决定的,
并不是说调用了start
方法之后线程就已经开始运行了,调用start
方法只是一个通知,有可能立刻就还会执行,也可能会等一会执行,
调用start
方法之后,是有两个线程在执行的,主线程以及开启的新线程准备工作
新线程在执行之前是需要做一些准备工作,首先会进入到就绪状态,此时已经获取到了除了cpu之外的其他资源,比如上下文内容等,
之后会被调度到执行状态,在执行状态才能去等待cpu资源获取cpu资源不能重复调用
start()
会抛出异常,如下图,一旦线程开始运行,就会更改状态,运行完毕就会更改为终止状态,无法在切换到其他状态,所以第二次执行就会抛出异常
start()
源码解析
启动新线程检查线程状态
如下图,可以看到start
方法中,第一步就是先去检查线程的状态是不是0
线程状态threadStatus字段初始值为0
加入线程组
如上面的图所示,可以看到,检查完线程状态,会加入有个group组group.add(this);
start0()
之后就会调用start0()
方法,start0()
方法是一个native
方法,是由C++实现的
run()
方法原理解读
直接调用run
方法,其实就是调用一个普通的方法,并不会启动新的线程,只是一个对象调用了一个普通的方法。
常见面试题
一个线程两次调用start()
方法,会出现什么情况?为什么?
刚刚上面已经讲解
既然start()
方法会调用run()
方法,为什么我们选择调用start()
方法,而不是直接调用run()
方法呢?
上面已经讲解
上山容易下山难——如何正确停止线程?
讲解原理
- 原理介绍:使用
interrupt
来通知,而不是强制interrupt是通过启用一个线程来通知另一个线程停止的操作,java中没有强制停止一个线程的机制,只能做到通知,
被停止的线程拥有最高权限,它可以自己判断是否要停止,
因为被停止的线程一般收到停止通知之后,会需要做一些收尾工作,比如数据回滚数据保存之类,所以通知线程只能是通知,无法做到强制。
最佳实践:如何正确停止线程
通常线程会在什么情况下停止
run
方法的所有代码都运行完毕,线程会自动停止- 有异常出现,并且方法中没有捕获异常会导致线程停止
正确的停止的方法:interrupt
通常线程会在什么情况下停止普通情况
代码如下所示,Thread.currentThread().isInterrupted()
方法是查看线程是否中断,需要不停大的在循环中查看线程状态,才能正确的停止线程,
否则的话线程是不会理会停止操作的。
1 | public class RightWayStopThreadWithoutSleep implements Runnable { |
线程可能被堵塞
代码如下所示,我们要做的就是让线程处于sleep
的时候收到中断通知
1 | public class RightWayStopThreadWithSleep { |
结果如图所示,当线程处于sleep的时候收到中断通知,会直接抛出异常
如果线程每次迭代后都堵塞
代码如下所示,在这种情况下,我们就没必须每次循环都调用Thread.currentThread().isInterrupted()
去检查线程是否已经中断,因为每次循环的时候都会进行sleep
,sleep
对中断做了处理
1 | public class RightWayStopThreadWithSleepEveryLoop { |
while
内try/catch
的问题
- 如果将
try/catch
放到循环内部的话,中断会失败,因为上面的代码其实是依靠异常来中断了循环,然后再被循环外面的try/catch
处理,而如果把try/catch
放到循环里面,就无法依靠异常中断循环,所以循环将会继续 - 把
try/catch
循环里面在while循环条件里面添加上Thread.currentThread().isInterrupted()
判断是否中断,这样也是无效的,因为线程在获取到一次标记之后,就会清除标记,所以如果sleep
获取了标记,Thread.currentThread().isInterrupted()
就无法再成功检查了(极小概率再sleep
之后,while
循环之前收到中断通知,这样再进行判断的话就会中断循环,但概率极小)
实际开发中的两种最佳实践
优先选择:传递中断
如代码中所示,如果我们在调用其他方法的时候,有sleep
的操作,那么不应该在方法里面捕获异常,而应该将异常上抛得到run
方法里面,否则run
方法里面无法感知到线程中断通知
1 | public class RightWayStopThreadInProd implements Runnable{ |
不想或无法传递:恢复中断
如下代码所示,如果下级方法想要自己处理异常,那么可以调用Thread.currentThread().interrupt()
将中断通知恢复,以便于上级run
方法感应到中断通知
1 | public class RightWayStopThreadInProd2 implements Runnable { |
不应该屏蔽中断
既不在方法签名中抛出,也不在捕获异常后重新恢复中断,这样会导致上级run
方法无法收到中断通知,以至于无法终止操作
响应中断的方法总结列表
- Object.wait() / wait(long) / wait(long, int)
- Thread.sleep(long) / sleep(long, int)
- Thread.join() / join(long) / join(long, int)
- java.util.concurrent.BlockingQueue.task() / put(E)
- java.util.concurrent.locks.Lock.lockInterruptibly()
- java.util.concurrent.CountDownLatch.await()
- java.util.concurrent.CyclicBarrier.await()
- java.util.concurrent.Exchanger.exchange(V)
- java.nio.channels.InterruptibleChannel 相关方法
- java.nio.channels.Selector相关方法
正确停止带来的好处
使用interrupt
来停止线程,可以由被停止的线程来判断要不要停止,也可以让被停止大的线程做好一些收尾工作
停止线程的错误方法
被弃用的stop
,suspend
和resume
方法
- stop方法会导致线程突然停止,这样对线程中运行的代码是非常不友好的,假如正在处理一个任务,想要撤销,有时候会需要对一些数据进行回滚,突然停止会导致数据无法进行处理
suspend
和resume
方法,挂起和唤醒,suspend
方法在挂起的时候并不会释放它已经持有大的锁,而如果需要唤醒它的线程需要获取锁才能唤醒,就会导致死锁
用volatile
设置boolean
标记位
看上去可行(有些时候确实是可用的)
代码如下,通过更改canceled
字段的值,来使线程停止
1 | public class WrongWayVolatile implements Runnable{ |
错误之处(有些时候会报错)
代码如下,生产者以较快的速度生产任务,消费者以较慢的速度消费任务,当队列满了之后,就会将生产者的异步堵塞,而这个时候,即便是更改canceled
字段的值,代码也无法检测到
1 | public class WrongWayVolatileCantStop { |
修正方案
修正后的代码如下,使用interrupt()
方法中断线程,并在循环的时候检查线程状态
1 | public class WrongWayVolatileFixed { |
重要函数的源码解析
interrupt()
方法
如图所示,interrupt()
方法中的主要逻辑都是调用interrupt0()
方法实现的,interrupt0()
方法是一个native方法
判断是否已被中断的相关方法
static boolean interrupted()
- 返回当前线程是否被中断,获取之后清除当前状态
- 这个静态方法的目标对象是调用它的线程
- 如下图,参数为true,表示要清除状态
boolean isInterrupt()
- 返回当前线程是否被中断,获取之后不清除状态
- 如下图,参数为false,表示不进行清除
Thread.interrupted()的目标对象
- 目标对象是调用它的线程,即便使用的是其他线程的对象调用的这个静态方法,得到的结果依然是当前线程的
- 如果再main主线程中调用,无论是使用Thread直接调用,还是通过开启的多线程对象调用,得到的结果都是主线程的结果。
常见面试问题
如何停止线程
- 原理:用
interrupt
来请求、好处(使用interrupt停止线程,只是通知线程需要中断,可以给线程预留处理后续数据的时间) - 想要停止线程,需要请求方、被停止方、子方法被调用方相互配合(请求方发出中断通知,被停止方需要在循环中监听中断状态,子方法需要将中断异常上抛,或者捕获后重新恢复中断状态)
- 最后再说错误的方法:
stop/suspend
已废弃,volatile
的boolean无法处理长时间阻塞的情况(stop
方法会直接停止线程,导致线程无法正确回滚数据;suspend
方法可能会导致死锁;volatile+boolean
的方法,在阻塞的情况下是无法中断线程的)
如何处理不可中断阻塞
- 没有通用的处理方法,只能根据不同的场景,尽量使用能够响应线程中断的方法
线程的一生——6个状态(生命周期)
有哪6中状态?每个状态是什么含义?
New
已创建还未启动
当我们使用new Thread()创建了一个线程,还没有执行start方法,这个时候这个线程就处于这个状态Runnable
可运行/运行中
一旦线程调用了start方法,就会处于这个状态,有可能这个时候线程调度器还没有分配线程进行执行(此时可以称为可运行状态)
cpu分配了资源,进入运行状态,此时为运行中,依然是Runnable状态(运行中)
如果cpu调度器将资源突然调度到其他地方,那么该线程依然为Runnable状态(可运行)Blocked
被阻塞
进入synchronized代码块并且没有拿到锁时,处于该状态
当线程拿到锁之后,会切换到Runnable状态执行
只有synchronized锁,会进入此种状态Waiting
不计时等待
当调用这几个方法的时候,会使线程进入当前状态:Object.wait() \ Thread.join() \ LockSupport.park()
当调用这几个方法时,会唤醒等待,使线程重新进入Runnable状态:Object.notify() \ Object.notifyAll() \ LockSupport.unpark()
注意:方法都是不带时间的,如果代理时间参数,就会进入到计时等待Timed Waiting
计时等待
调用这几个方法,会进入当前状态:Thread.sleep(time) \ Object.wait(time) \ Thread.join(time) \ LockSupport.parkNanos(time) \ LockSupport.parkUntil(time)
当达到等待时间,或者调用这几个方法,会唤醒线程进入Runnable状态:Object.notify() \ Object.notifyAll() \ LockSupport.unpark()Terminated
已终止
当run方法已经执行完毕,会进入当前状态
或者抛出一个未捕获的异常,也会进入当前状态
状态间的转化图示
状态之间转换代码演示
- NEW -> RUNNABLE -> RUNNABLE -> TERMINATED
代码未执行start方法时状态为 NEW
代码执行start方法后状态为 RUNNABLE (此时无法分辨是还未调用cpu资源还是已经调用cpu资源)
run在循环中,状态为 RUNNABLE
run方法运行结束后,状态为 TERMINATED
1 | public class NewRunnableTerminated implements Runnable{ |
- TIMED_WAITING -> BLOCKED -> WAITING
线程1进入了
syn()
方法,执行了休眠1秒,此时的状态为 TIMED_WAITING
因为syn()
方法是synchronized
修饰的,线程1进入之后,线程2无法再拿到锁,所以此时线程2的状态为 BLOCKED
线程1休眠1秒种之后,进入wait()
方法,此时状态为 WAITING1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class BlockedWaitingTimedWaiting implements Runnable {
public static void main(String[] args) throws InterruptedException {
BlockedWaitingTimedWaiting runnable = new BlockedWaitingTimedWaiting();
Thread thread1 = new Thread(runnable);
thread1.start();
Thread thread2 = new Thread(runnable);
thread2.start();
Thread.sleep(10);
System.out.println(thread1.getState());
System.out.println(thread2.getState());
Thread.sleep(1200);
System.out.println(thread1.getState());
}
public void run() {
syn();
}
private synchronized void syn() {
try {
Thread.sleep(1000);
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
阻塞状态是什么?
- 一般习惯而言,把
Blocked
(被阻塞)、Waiting
(等待)、Timed_Waiting
(计时等待)都称为阻塞状态 - 不仅仅是
Blocked
常见面试问题
线程有哪几种状态?生命周期是什么?
具体回答可以根据上面的线程状态流转图进行回答
Thread和Object类中的重要方法详解
方法概览
类 | 方法名 | 简介 |
---|---|---|
Thread | sleep相关 | 本表格的”相关”,指的是重载方法,也就是方法名相同,但参数个数不同,例如sleep有多个方法,只是参数不同,实际作用大同小异 |
join | 等待其他线程执行完毕 | |
yield相关 | 放弃已经获取到的cpu资源 | |
currentThread | 获取当前执行线程的引用 | |
start,ren相关 | 启动线程相关 | |
interrupt相关 | 中断线程 | |
stop(),suspend(),resuem()相关 | 已废弃 | |
Object | wait/notify/notifyAll相关 | 让线程暂停时休息和唤醒 |
wait
,notify
,notifyAll
方法详解
作用,用法
阻塞阶段
- 执行
wait
方法进入阻塞阶段的时候,必须要拥有monitor锁 - 进入阻塞阶段之后,直到以下4种情况之一发生时,才会被唤醒
1)另一个线程调用这个对象的
notify()
方法且刚好被唤醒的是本线程
2)另一个线程调用这个对象的notifyAll()
方法
3)过了wait(long timeout)
规定的超时时间,如果传入0就是永远等待
4)线程自身调用了interrupt()
唤醒阶段
notify()
,随机唤醒一个线程notifyAll()
,唤醒所有的线程
遇到中断
- 会抛出
InterruptedException
异常,然后释放锁
代码演示:4种情况
普通用法
代码如下所示
我们先启动线程1,使线程1先进入到synchronized代码块中,并且已经执行了wait方法,然后在启动线程2
由打印结果我们可以看出来,线程1在执行了wait方法之后,是已经释放锁了的,不然线程2无法进入同步代码块执行notify方法,也无法将线程1唤醒
由结果打印顺序也可以看出,我们是先运行了线程1,打印线程已经执行,然后线程2调用notify方法,之后打印输出,然后线程1再打印输出
所以,线程2将线程1唤醒之后,线程1是处于阻塞状态,不会立即执行代码,而是会等线程2执行完毕,线程1才能再次获取锁,然后开始执行
1 | public class Wait { |
打印结果
1 | Thread-0已经开始执行了 |
notify和notifyAll展示
代码如下所示
线程A和线程B同时启动,然后串行执行同步代码块,线程A执行到wait方法,释放锁,线程B开始执行,线程B执行到wait方法后,释放锁
等待200毫秒之后,线程C开始执行,线程C执行了notifyAll
方法,唤醒了阻塞中的线程A和线程B,然后线程A和线程B执行结束
如果使用的是notify
方法的话,那么线程A和线程B只能唤醒一个,另一个将进行无尽的等待
1 | public class WaitNotifyAll implements Runnable { |
运行结果:
1 | Thread-0获取到锁 |
只释放当前monitor展示
创建两把锁,A和B
线程1先获取锁A,然后获取锁B,然后释放锁A
线程2先获取锁A,然后尝试获取锁B
从结果可以看出,线程2是无法获取锁B的,因为线程1只释放了锁A
只释放当前monitor,不会影响已经拿到的其他monitor
1 | public class WaitNotifyReleaseOwnMonitor { |
运行结果:
1 | 线程1获取到resourceA锁 |
特点,性质
- 调用wait或者notify或者notifyAll必须先拥有monitor(否则还会抛异常:IllegalMonitorStateException)
- notify只能唤醒一个,这一个是随机唤醒,无法指定
- 这三个方法都属于Object类,所以所有对象都可以调用这三个方法
- 类似功能的Condition(依赖与Lock锁)
- 同时持有多把锁大的情况(释放锁的时候要注意是多把锁,需要避免死锁)
手写生产者消费者设计模式
代码如下所示,使用
wait
和notify
实现生产者消费者
1 | public class ProducerConsumerModel { |
常见面试题
用程序实现两个线程交替打印0~100的奇偶数
基本思路:
synchronized
使用synchronized实现代码如下
使用位移运算符判断奇偶数进行打印,打印后自增1
两个线程使用同一把锁,如果线程抢到了锁,判断不符合自己的条件,就只是空循环,打印也不会加1
这个方法的问题就是可能会等导致线程多次抢到锁之后空转1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class WaitNotifyPrintOddEvenSyn {
private static int count;
private static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 0) {
System.out.println(Thread.currentThread().getName() + ": " + count++);
}
}
}
}
}, "偶数").start();
new Thread(new Runnable() {
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 1) {
System.out.println(Thread.currentThread().getName() + ": " + count++);
}
}
}
}
}, "奇数").start();
}
}更好的办法:
wait/notify
使用wait/notify的代码如下
线程打印完之后,会去唤醒另外一个线程,然后自己进行休眠,线程交替唤醒交替休眠,第一个启动的线程为偶数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class WaitNotifyPrintOddEvenWait {
static int count ;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new TurningRunner(),"偶数").start();
new Thread(new TurningRunner(),"奇数").start();
}
static class TurningRunner implements Runnable{
public void run() {
while (count <= 100){
synchronized (lock){
System.out.println(Thread.currentThread().getName()+":"+count++);
lock.notify();
if (count <= 100){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
手写生产者消费者设计模式
- 上面已经有了具体实现方法
为什么wait
需要再同步代码块中使用,而sleep
不需要
java语言设计的时候为了防止死锁
假设一个线程在执行代码,后面是调用wait
方法,另一个线程后面是notify
方法,如果不能同步的话,可能会存在执行到wait
之前,先执行了notify
方法,这样就会导致第一个线程在调用了wait之后陷入永久等待
而sleep
所影响的只是当前线程所以可以不在同步代码块中
为什么线程通信的方法wait()、notify()、notifyAll()
被定义在Object
类里面?而sleep
定义在Thread
类里?
因为
wait()、notify()、notifyAll()
方法是属于锁级别的,而每一个锁都是一个对象
java实现锁的原理也是在对象头中做标记
如果将这三个方法放到每个线程中,每个线程也可能会持有多把锁,这样每次休眠和唤醒会非常麻烦
wait
方法是属于Object
对象的,那调用Thread.wait
会怎么样?
Thread
也可以做锁对象,但是Thread
比较特殊,最好不要用它来做锁对象
在线程退出的时候,会调用Thread
类的notifyAll
方法,所以如果使用Thread
类做锁对象可能会导致线程总是被无故唤醒
如何选择用notify
还是notifyAll
主要看是像唤醒多个线程还是一个线程,根据具体情况进行选择
notifyAll
之后所有的线程都会再次抢夺锁,如果某个线程抢夺失败怎么办?
其他被唤醒但是没有获得锁的线程,会陷入阻塞状态,继续等待拿到锁
用suspend()
和resume()
来阻塞线程可以吗?为什么?
上面已经做出了解释
sleep
方法详解
- 作用:我只想让线程在预期的时间执行,其他时候不要占用CPU资源
sleep
方法不释放锁
- 包括
synchronized
和lock
- 和
wait
不同
sleep
方法响应中断
- 抛出
InterruptedException
异常 - 清理中断状态
代码如下,在sleep中被中断,还会抛出异常,然后继续执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class SleepInterrupted implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SleepInterrupted());
thread.start();
Thread.sleep(6500);
thread.interrupt();
}
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("我被中断了");
e.printStackTrace();
}
}
}
}
总结
- sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常并清除中断状态
常见面试题
wait/notify、sleep异同(方法属于那个对象?线程状态怎么切换?)
相同
阻塞
响应中断不同
同步方法
释放锁
指定时间
所属类
join方法
- 作用:因为新的线程加入了我们,所以我们要等待他执行完再出发
- 用法:main等待thread1执行完毕,注意谁等谁
普通用法
执行join之后,主线程会进行等待,等子线程执行完毕之后才会继续运行后面的代码
1 | public class Join { |
遇到中断
join抓取的中断异常是主线程的中断异常
1 | public class JoinInterrupted { |
在join期间,线程是什么状态?:waiting
join期间,主线程处于
Waiting
状态
1 | public class JoinThreadState { |
- CountDownLatch或CyclicBarrier是经过封装的,可以实现和join一样的功能
join源码
由图可以看出,join方法也是通过wait实现的
而在Thread线程结束后,会调用一次notifyAll方法,唤醒线程,具体代码在C++部分
这也不能用Thread类做锁的原因
线程退出源码
join等价代码
可以使用wait来进行替换,上面的源码已经显示,线程在退出的时候,会唤醒所有的线程,所以在线程运行完毕之后,会唤醒wait的休眠
1 | public class JoinPrinciple { |
面试常见问题
在join期间,线程处于哪种线程状态?
- 处于Waiting状态
yield方法
- 作用:释放我的CPU时间片(释放时间片之后,状态依然是Runnable状态,随时有可能会被CPU调度执行)
- 定位:JVM不保证遵循(根据具体的情况,有可能不生效,不同的cpu实现原理也可能会导致有不同的效果)
- yield和sleep的区别:是否随时可能再次被调度(sleep线程认为是阻塞的,不会再调度起来,yield则随时可能被调度起来)
获取当前执行线程的引用:Thread.currentThread()方法
- 返回当前所执行的线程的引用
start和run方法
- 前面已经有介绍
- start会启动一个线程,run则只是调用对象的普通方法
stop,suspend,resume方法
- 已弃用
线程的各个属性
属性名称 | 用途 | 注意事项 |
---|---|---|
编号(ID) | 每个线程都有自己的ID,用于标识不同的线程 | 被后续创建大的线程使用;唯一性;不允许被修改 |
名称(Name) | 作用让用户或者程序员在开发、调试或运行过程中,更容易区分不同的线程、定位问题等。 | 清晰有意义的名字;默认的名字 |
是否是守护线程(isDaemon) | true代表该线程是【守护线程】,false代表线程是非守护线程,也就是【用户线程】 | 二选一;继承父线程;setDaemon |
优先级(Priority) | 优先级这个属性的目的是告诉线程调度器,用户希望哪些线程相对多运行、那些少运行 | 默认和父线程的优先级相等;共10个等级;默认值是5;不应该依赖 |
线程Id
- 每个线程都有一个Id
- 线程Id是不能修改的
- 线程的Id是自增的,从1开始,main线程是第一个Id
- 但是在main方法里面创建的第一个线程绝对不是2,因为jvm启动大的时候会创建一些其他的线程
- Thread类中获取Id的方法入下,
threadSeqNumber
初始为0,因为++
在前,所以第一个线程的Id是1
线程名字
- 线程的名称如果没有设置默认是’Thread_0’,后面的数字递增
- 创建线程如果没有设置名称,会如下图设置一个默认名称,名称后面的编号是递增,从0开始
- 线程名称如下图,会设置两个名称,一个线程在java层面的名称,一个是在native的名称
- 设置名称时会判断线程状态,如果线程已经不是未启动状态,无法修改native层面的名称
守护线程
守护线程和普通线程的区别
- 整体无区别(都是为了执行任务,不过有的是用户的任务,有的是JVM的任务)
- 唯一的区别在于JVM的离开(用户线程会影响JVM是否退出,守护线程不会影响)
常见面试问题
守护线程和普通线程的区别
答案同上
我们是否需要给线程设置为守护线程
不应该设置为守护线程,因为如果现在在处理数据,设置成守护线程,JVM退出的时候不会考虑它是否还在运行
线程优先级
- 优先级一共由10个级别,线程的优先级默认是5
- 程序设计不应该依赖于优先级
不同的操作系统对优先级的理解是不一样的(每种操作系统的优先级级别设置是不一样的,需要一些映射,这样可能会和原本的期望不一致)
优先级会被操作系统修改()
未捕获异常如何处理?
线程未捕获异常UncaughtException
应该如何处理?
- 可以使用
UncaughtExceptionHandler
类来进行处理
为什么要用UncaughtExceptionHandler
处理?
- 主线程可以轻松发现异常,子线程却不行
- 子线程的异常无法使用传统办法捕获
- 如果不对子线程的异常进行处理,那么会导致线程中断,任务无法处理,不利于程序的健壮性
两种解决办法
- 方案一(不推荐):手动在每个run方法里面进行try/catch
- 方案二(推荐):利用
UncaughtExceptionHandler
UncaughtExceptionHandler接口
异常处理器的调用策略
- 默认线程处理器(ThreadGroup)逻辑如下代码实现异常捕获器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public void uncaughtException(Thread t, Throwable e) {
//默认情况下parent是null
if (parent != null) {
parent.uncaughtException(t, e);
} else {
//调用Thread.setDefaultUncaughtExceptionHandler(...)
//方法设置的全局handler进行处理
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
//全局handler也不存在就输出异常栈
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
新建捕获器:具体使用:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private String name;
public MyUncaughtExceptionHandler(String name) {
this.name = name;
}
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.WARNING, "线程异常,终止了" + t.getName(), e);
System.out.println(name + "捕获了异常" + t.getName() + "异常:" + e);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class UseOwnUncaughtExceptionHandler implements Runnable{
public static void main(String[] args) throws InterruptedException {
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));
new Thread(new CantCatchDirectly(),"MyThread-1").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(),"MyThread-2").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(),"MyThread-3").start();
Thread.sleep(300);
new Thread(new CantCatchDirectly(),"MyThread-4").start();
}
public void run() {
throw new RuntimeException();
}
}常见面试问题
Java异常体系
如何全局处理异常?为什么要全局处理异常?不处理行不行?
上面已经有了讲解run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
- run方法不能再向上抛出异常
- 如果是可检查异常,只能再run方法中进行捕获处理
- 如果是不可检查异常,抛出后,线程会终止运行
线程中如何处理某个未处理异常
- UncaughtExceptionHandler
双刃剑:多线程会导致的问题
线程安全
什么是线程安全
不管业务中遇到怎样的多个线程访问某个对象或者方法的情况,而再编译这个业务逻辑的时候,都不需要额外等地的做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错,就可以称为线程安全)
什么情况下会出现线程安全问题,怎么避免?
运行结果错误:a++多线程下出现消失的请求现象
最终代码演示如下所示
1 | public class MultiThreadError implements Runnable { |
活跃性问题:死锁、活锁、饥饿
死锁示例代码如下
1 | public class MultiThreadError implements Runnable{ |
对象发布和初始化的时候的安全问题
什么是发布
- 对象可以在超过当前类的范围外使用
- public修饰的属性对象
- 一个方法return出一个对象
- 将一个对象作为参数传到其他类的方法中
什么是逸出
- 1.方法返回一个private对象(private的本意是不让外部访问)
- 2.还未完成初始化(构造函数没有完全执行完毕)就把对象提供给外界,比如:
- 在构造函数中未初始化完毕就this赋值
- 隐式逸出——注册监听事件
- 构造函数中运行线程
各种需要考虑线程安全的情况
- 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
- 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check0then-act
- 不同的数据之间存在捆绑关系的时候
- 我们使用其他类的时候,如果对方没有声明自己是线程安全的
为什么多线程会带来性能问题
调度:上下文切换
- 主要是值当线程发生调度的时候
- 当可运行的线程数超过了CPU的核数,就会开始调度线程,以便于让每个线程都有机会运行
什么是上下文
- 主要和一些程序计数器相关
- 切换的时候首先需要挂起一个线程,将线程的运行状态保存得到内存中的某处
- 包括线程运行时需要的一些数据
缓存开销
- CPU为了运行速度,会先在通过计算,在内存中缓存一些可能用到的数据
- 而进行了上下文切换之后,这些缓存的数据大部分都已经没用了
何时会导致密集的上下文切换
- 频繁的抢锁,频繁的IO操作
协作:内存同步
- 线程在运行的时候,会在线程内部缓存一些数据,如果使用锁,会强制同步线程的缓存数据和主内存的数据
- 使线程缓存数据失效,只能使用主内存数据