面试题六:并发
-
- 使用CountDownLatch实现ABC三个线程顺序执行
// 资源类
class MyWorker implements Runnable
{
CountDownLatch countDownLatch1;
CountDownLatch countDownLatch2;
MyWorker(CountDownLatch c)
{
this.countDownLatch1 = c;
}
MyWorker(CountDownLatch c1, CountDownLatch c2)
{
this.countDownLatch1 = c1;
this.countDownLatch2 = c2;
}
@Override
public void run() {
try {
countDownLatch1.await();
if (countDownLatch2!=null) {
countDownLatch2.countDown();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程"+Thread.currentThread().getName()+"执行完成");
}
}
/**
* 使用CountDownLatch顺序执行ABC三个线程
*/
private static void excuteThreadWithSort() throws Exception{
CountDownLatch ca = new CountDownLatch(1);
CountDownLatch cb = new CountDownLatch(1);
CountDownLatch cc = new CountDownLatch(1);
new Thread(new MyWorker(ca, cb), "AAA").start();
new Thread(new MyWorker(cb, cc), "BBB").start();
new Thread(new MyWorker(cc), "CCC").start();
TimeUnit.SECONDS.sleep(1);
ca.countDown();
System.out.println("main线程执行结束");
}
-
- ThreadLocal相关
- ThreadLocal相关
-
- 线程池的工作原理,几个重要参数,然后给了几个具体参数分析线程池会怎么做,最后问阻塞队列的作用是什么?
工作流程图
-
- 讲一讲为什么AtomicInteger要用CAS而不是synchronized?
1.synchronized是悲观锁,是一种独占锁,这也就意味着同时只能有一个线程持有资源,其他线程只能阻塞来等待释放锁。这是必会导致线程上下文切换,在线程数量较多时,会导致CPU频繁的上下文切换,从而导致效率较低;
2.CAS采用的是乐观锁,也就是假设从来不会阻塞任何线程,这就不会导致线程的上下文切换,所以在效率上要比synchronized要高
-
- 单机上一个线程池正在处理服务如果忽然断电怎么办?(正在处理和阻塞队列里边的请求怎么处理)
我们可以对正在处理和阻塞队列的任务做事物管理或者对阻塞队列中的任务持久化处理,并且当断电或者系统崩溃,操作无法继续下去的时候,可以通过回溯日志的方式来撤销正在处理的已经执行成功的操作。然后重新执行整个阻塞队列。阻塞队列持久化,正在处理事物控制。断电之后正在处理的回滚,日志恢复该次操作。服务器重启后阻塞队列中的数据再加载
-
- 使用无界阻塞队列会出现什么问题?
OOM
-
- synchronized和lock有什么区别?synchronized什么时候是对象锁?什么时候是全局锁?为什么?
区别:
- 原始构成层面:synchronized是关键字属于jvm层面,底层依赖moniter对象来完成加锁和解锁,lock是api层面的锁,是具体类;
- 使用方法: synchronized不需要手动释放锁,代码执行完成后系统自动让线程释放对锁的占用;Lock需要手动释放锁;
- 等待是否可中断: synchronized不可以中断,除非抛出异常或者执行完成;Lock可以中断,使用tryLock或者lockInterruptibly()放代码块中,调用interrupt()方法中断;
- 加锁是否公平:sunchronized是非公平锁,Lock默认是非公平锁,可以通过参数设置为公平锁;
- 锁绑定多个条件: sunchronized没有,ReenTrantLock用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不像synchronized那样要不唤醒一个要不唤醒全部。
-
- ThreadLocal底层是如何实现的?写一个例子
ThreadLocal是为每个线程创建单独的变量副本,故而每个线程都可以单独的改变自己的变量副本而不会影响其他的线程。
底层实现原理:1.ThreadLocal类中有一个ThreadLocalMap的内部类,key值是当前ThreadLocal,valu是当前线程中的变量副本,get()\set()\remove()方法都是基于该内部类进行操作;
2.Thread、ThreadLocal、ThreadLocalMap的关系:
Thread类中有一个ThreadLocalMap属性,ThreadLocal又是ThreadLocalMap的key值;
且ThreadLocalMap的getEntry()以及setEntr的key值是弱引用,防止内存泄漏,但是在key值被回收后,由于value不一定会被回收,因此还是存在内存泄漏的可能性,虽然ThreadLocalMap的getEntry()以及setEntry()已经在key==null时将value设置为null,但是由于value存在和Thread对象的强引用,因此未必会被回收,所以需要再使用完以后手动调用一下ThreadLocal的remove()方法。
内存泄漏示意图:
ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。
每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本;
ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。
例子:
public class ThreadLocalTest {
private static ThreadLocal<Integer> seq1 = ThreadLocal.withInitial(() -> 0);
public static Integer incr() {
seq1.set(seq1.get() + 1);
return seq1.get();
}
public static void main(String[] args) {
System.out.println("开始执行main方法");
new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("线程"+Thread.currentThread().getName()+" object>>>"+incr());
}
}, "AAA").start();
System.out.println("main方法执行完成");
new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("线程"+Thread.currentThread().getName()+" object>>>"+incr());
}
}, "BBB" + j).start();
}
}
-
- volatile的工作原理,指令重排序有什么意义?原子性问题,为什么i++不支持原子性?从计算机原理的设计来讲下不能保证原子性的原因?
代码示例:
public class VolatileTest {
public static volatile int temp = 0;
public static void main(String[] args) {
new Thread(()->{
while (temp!=1){}
System.out.println("线程"+Thread.currentThread().getName()+"执行完成,此时temp的值:"+temp);
},"t1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(2);
temp = 1;
System.out.println("线程"+Thread.currentThread().getName()+"设置temp的值为:"+temp);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2").start();
}
}
volatile工作原理:
volatile在多线程环境下能够保证可见性以及一定的有序性,但是不能保证原子性。在JVM底层,volatile是使用内存屏障来实现的。也就是volatile保证可见性,不保证原子性,禁止指令重排;
指令重排的意义:
在执行程序时,为了提高性能,编译器和处理器会对指令进行重排,在指令重排时需要遵循happens-before规则:
1.编译器重排。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
2.处理器重排。如果不存在数据依赖性,处理器可以改变语句对应机器指令的顺序;
指令重排在单线程下对程序没有影响,但是在多线程环境下会影响多线程执行的正确性,因此需要使用volatile禁止指令重排,查看加了volatile和没加volatile的程序编译成的汇编语言可以发现,加了volatile时会多出一个Lock前缀指令,相当于一个内存屏障,内存屏障是一组指令,用来实现对内存操作的顺序限制。
内存屏障指令如下所示:
i++为什么不支持原子性?从计算机设计原理分析?
在java中,只有对int、boolean、byte、short等基本数据类型赋值时,jvm保证其为原子性,在32位操作系统下,long和double都不是原子操作。i++操作属于复合操作,具体的操作步骤:
1.从主存中读取到i的值,复制到CPU的高速缓存中;
2.cpu执行+1操作,CPU将执行结果写回到告诉缓存中;
3.将执行结果从高速缓存刷新到主存中;
也就是说i++操作其实是分为三个步骤,此时如果有多个线程执行该操作,在没有同步的情况下就会导致出现预期之外的结果,也就是缓存一致性问题;
解决缓存一致性的两种方案:
1.总线加锁
2.缓存一致性协议
总线加锁的方式采用独占的方式,只能一个CPU能够运行,效率比较低下;
缓存一致性协议(MESI)确保每个高速缓存中的变量副本是一致的,其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行为是无效的,因此其他CPU在读取数据时发现其缓存无效会重新从主存中读取。
示意图如下
-
- cas知道吗?如何实现的?
概念:
CAS比较并交换,JUC中的AQS以及原子类等都是基于CAS来实现,包括jdk1.8的concurrentHashMap也是基于CAS+synchronized来实现的。
CAS底层使用sun.misc.UnSafe
类的相关方法,共有三个参数,分别是内存值A、期望的原始值B、要更新的值C,只有当A==B的时候才会将内存值修改为C,否则什么也不干;
UnSafe
类提供了硬件级别的原子操作,通过变量的内存偏移地址获取到变量的原始值,另外变量值使用volatile修饰,保证变量的可见性。
原理:
CAS能够保证读-改-写操作在多线程环境下是原子操作,CPU使用了2种方式来实现,分别是总线加锁和缓存加锁:总线加锁
总线加锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器就能独占使用共享内存;缺点就是阻断了内存和处理器的通道,处理器也不能处理其他内存地址的数据,开销比较大。
缓存加锁
针对上面的情况,我们只需要保证在某一时刻,对某个内存地址的操作时原子性的就可以。缓存加锁,就是缓存在内存区域的数据如果再加锁期间,当他执行写操作写回内存时,处理器不再输出LOCK#信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证一个内存区域的数据仅能被一个处理器修改。
缺陷:
1.循环时间太长
2.只能保证一个共享变量的原子操作
3.ABA问题
-
- 并发包里面的原子类有哪些,怎么实现?cas在cpu级别用什么指令实现的?
分为四种类型:原子更新基本类型、原子更新数组类型、原子更新引用类型、原子更新字段类型
-
- java中有哪几种锁?手写一个自旋锁?
- java中有哪几种锁?手写一个自旋锁?
// 自旋锁样例
public class SpinLock {
AtomicReference<Thread> reference = new AtomicReference<>();
public void lock(){
Thread t = Thread.currentThread();
while(!reference.compareAndSet(null,t)){
//TODO 实现业务逻辑
}
}
public void unlock(){
Thread t = Thread.currentThread();
while(!reference.compareAndSet(t,null)){
//TODO 实现业务逻辑
}
}
}
-
- synchronized实现原理?
synchronized可以保证同步块或者同步方法在运行时,同一时刻只有一个方法可以进入临界区,同时它还可以保证共享变量的内存可见性。
java里边每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前class对象
- 同步方法块,锁是括号里面的对象
实现原理:
利用javap工具查看生成的class文件信息可以看到,同步代码块是使用monitorenter和monitorexit指令来实现的,同步方法依靠的是方法修饰符上的ACC_SYNCHRONIZED实现的。
同步代码块:monitorenter插入到同步代码块开始的位置,monitorexit指令插入到同步代码块结束的位置,jvm需要保证每个monitorexter指令都有一个monitorexit指令与之对应。任何一个对象都有一个monitor与之相关联,当且一个monitor被持有之后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,也就是尝试获取锁;
同步方法:synchronized将被翻译成普通的方法调用和返回指令,在vm字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在class文件的方法表中将该方法的access_flag字段中的synchronized标志位置为1,表示该方法是同步方法,并使用调用该方法的对象或该方法所属的class在jvm的内部对象表示KClass作为锁对象。
对象头:
synchronized使用的锁是存在于对象头里边的,Hotspot虚拟机的对象头主要包含两部分:
Mark Word:
用来存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
Mark Word存储的内容包含哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。如下图所示:
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间存储效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽可能多的数据,他会根据对象的状态复用自身的存储空间,也就是说Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机):
未开启指针压缩的情况下,32位系统中,Mark Word部分占了4bytes,也就是32bits,在64位系统中,Mark Word部分占了8bytes,也就是64bits。
Kclass Pointer:
对象指向它的类元数据的指针,虚拟机通过这个指针确定对象是哪个类的实例。
未开启指针压缩的情况下,32位系统中,Kclass Pointer部分占了4bytes,也就是32bits,在64位系统中,Kclass Pointer部分占了8bytes,也就是64bits。
monitor:
通常被描述为一个对象,也可以描述为一种同步机制或者同步工具。所有的java对象都是天生的monitor,monitor是线程私有的数据结构,每一个线程都有一个可用的monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
-
- ReentrantLock实现原理?
ReentrantLock底层是AQS
AQS原理图
-
- ArrayBlockingQueue内部如何实现的?LinkedBlockingQueue内部如何实现的?
- ArrayBlockingQueue内部如何实现的?LinkedBlockingQueue内部如何实现的?
-
- 线程间如何通信?
1.等待唤醒机制
wait()/notify()或者notifyAll()
await()/signal()或者signalAll()
park()/unpark()
2.共享变量
使用volatile修饰,保证可见性、有序性、禁止指令排序
-
- concurrentHashMap如何做到线程安全的?
参考4-17问题
-
- 如何保证线程安全?
1.synchronized
2.Lock接口
3.volatile+CAS
4.原子类
5.线程安全容器
-
- happens before原理?
happens-before原则保证了程序的有序性,它规定两个操作的执行顺序如果不能从happens-before原则推到出来,那就不能保证它们执行有序性,可以随意进行重排序
happens-before规则如下所示:1.程序次序结构:一个线程内,按照代码书写顺序,书写在前面的操作,happens-before于写在后面的程序;
2.锁定规则: 一个unLock操作,happens-before于对于该锁的Lock操作;
3.volatile 变量规则: 对一个变量的写操作,happens-before于后面对这个变量的读操作;
4.传递规则: 如果A操作happens-before于B操作,B操作happens-before于C操作,那么A操作happens-before于C操作;
5.线程启动规则: thread对象的start方法,happens-before于该线程的每一个动作;
6.线程中断规则:对线程的interrupt()操作,happens-before于被中断线程的代码检测到中断事件的发生;
7.线程终结规则: 线程中所有的操作,都happens-before线程的终止检测,我们可以通过Thread.join()结束Thread.isAlive()返回值的手段,检测到线程已经终止;
8.对象终结规则: 一个对象的初始化完成,happens-before于这个对象的finalize()方法针对上述8条原生的happens-before原则,还可以推导出其他的规则:
1.将一个元素放入一个线程安全的队列的操作,happens-before 从队列中取出这个元素的操作;
2.将一个元素放入一个线程安全容器的操作,happens-before 从容器中取出这个元素的操作;
3.在 CountDownLatch 上的 countDown 操作,happens-before CountDownLatch 上的 await 操作;
4.释放 Semaphore 上的 release 的操作,happens-before 上的 acquire 操作;
5.Future 表示的任务的所有操作,happens-before Future 上的 get 操作;
6.向 Executor 提交一个 Runnable 或 Callable 的操作,happens-before 任务开始执行操作总结起来就是,如果两个操作不能从上述14条规则中推导出来,那么这两个操作就没有顺序的保障,JVM可以对这两个操作进行重排序。如果A操作happens-before于B操作,那么A操作在内存上所做的操作对B操作都是可见的。
-
- 公平锁和非公平锁?
公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
-
- java读写锁,以及主要解决什么问题?
在大多数场景下,对同一资源的读操作较多,写操作较少,此时如果使用ReenTrantLock,那么读和读之间也存在竞争,这势必导致性能下降,因此就有了读写锁,读和读之间通过共享锁实现,在同一时间可以允许多个读操作同时访问;但是写锁为排他锁,也即在写数据时,其他的写线程和读线程都阻塞。
读写锁特性:公平性:支持公平性和非公平性
重入性:读锁和写锁支持65535个递归读取和写入;
锁降级:遵循获取写锁、获取读锁以及最后释放写锁的次序,如此写锁能够降级成为读锁。
-
- AtomicInteger的ABA问题?原子更新引用了解吗?
假设存在变量A=100,线程1将变量值修改为110后再修改为100,此时线程2再执行修改100到120是能成功的,这就是ABA问题,解决方案是使用
AtomicStampedReference
添加版本号,样例代码如下:
//AtomicInteger的ABA问题演示
AtomicInteger atomicInteger = new AtomicInteger(100);
Thread t1 = new Thread(()->{
atomicInteger.compareAndSet(100,110);
System.out.println("线程"+Thread.currentThread().getName()+"第一次修改后的atomicInteger的值"+atomicInteger.get());
atomicInteger.compareAndSet(110, 100);
System.out.println("线程"+Thread.currentThread().getName()+"第二次修改后的atomicInteger的值"+atomicInteger.get());
},"t1");
Thread t2 = new Thread(()->{
try {
//暂停2秒等待线程t1执行完成
TimeUnit.SECONDS.sleep(2);
boolean flag = atomicInteger.compareAndSet(100,120);
System.out.println("线程"+Thread.currentThread().getName()+"是否修改成功:"+flag);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("atomicInteger的最终值:"+atomicInteger.get());
使用AtomicStampedReference解决ABA问题,代码演示:
AtomicStampedReference<Integer> reference = new AtomicStampedReference<Integer>(100,1);
Thread t1 = new Thread(()->{
try {
// 暂停1秒让线程2先获取到版本号
TimeUnit.SECONDS.sleep(1);
reference.compareAndSet(100,110,reference.getStamp(),reference.getStamp()+1);
System.out.println("线程"+Thread.currentThread().getName()+"第一次修改后的值:"+reference.getReference()+",版本号:"+reference.getStamp());
reference.compareAndSet(110,100,reference.getStamp(),reference.getStamp()+1);
System.out.println("线程"+Thread.currentThread().getName()+"第二次修改后的值:"+reference.getReference()+",版本号:"+reference.getStamp());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
Thread t2 = new Thread(()->{
try {
int stamp = reference.getStamp();
//暂停2秒,让线程1执行完,修改版本号
TimeUnit.SECONDS.sleep(2);
boolean flag = reference.compareAndSet(100,120, stamp, stamp+1);
System.out.println("线程"+Thread.currentThread().getName()+"修改成功:"+flag);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2");
t1.start();
t2.start();
-
- CountDownLatch、CyclicBarrier、Semaphore实现原理?
以上三者底层实现都是基于AQS
CountDownLatch(秦灭6国一统天下):state的值为初始化的值,每执行一次
countDown()
方法,state值减一,直到state==0时结束阻塞,执行最终线程,不能重用CyclicBarrier(集齐7颗龙珠召唤神龙):
底层由ReenTrantLock实现,当全部线程都达到某个点时才会放行执行最终的线程,只要还有一个线程没有到达公共点,那么其他线程就使用
condition.await()
等待,当所有线程都到达公共点后,调用condition.signalAll()
唤醒所有线程,并且开启新的一代(重用)Semaphore信号量(抢车位):
底层由AQS实现,每次给定state数量的许可,设置state值为许可证的数量,使用共享锁的方式,每来一个线程请求许可时,获取state的值,减去请求的许可的数量,当剩余的数量小于0或者cas设置剩余许可数量到state为false时,当前线程进入CLH队列阻塞知道有现成释放许可,当前线程才可能从队列中被唤醒。
使用:1.用于多个共享资源的互斥使用
2.并发线程数的控制
-
- 阻塞队列知道吗?为什么用?有什么好处?用在哪里?
阻塞队列其实就是一个队列,当队列为空时从队列中获取元素的操作将会被阻塞,当队列满了时向队列中添加元素会阻塞;
在JUC包发布之前,在多线程环境下,我们每个程序猿都需要自己控制阻塞唤醒的细节,尤其是还需要兼顾效率和线程安全,这会很大程度的增加程序的复杂性;
好处是我们不需要关心什么时候该阻塞,什么时候需要唤醒线程,因为这一切BlockingQueu都已经包办了;
-
- 死锁编码以及定位分析
产生死锁的原因:
- 系统资源不足
- 资源分配不当
- 进程/线程运行推进的顺序不合适
产生死锁的条件:
- 互斥条件
- 请求于保持条件
- 不可剥夺条件
- 循环等待条件
处理死锁的方式:
- jps命令定位进程号;
- jstack找到死锁查看;
样例代码:
public static void main(String[] args) { Object lock1 = new Object(); Object lock2 = new Object(); new Thread(()->{ synchronized (lock1){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2){ } } },"t1").start(); new Thread(()->{ synchronized (lock2){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1){ } } },"t2").start(); }
jstack查找原因:
-
- 线程池里的线程真的有核心线程和非核心线程之分?
没有,不管是核心线程或者非核心线程,哪个任务异常了或者正常退出了都会执行
ThreadPoolExecutor#processWorkerExit()
方法,根据实际情况将线程数减1;
-
- 线程池被 shutdown 后,还能产生新的线程?
可以,当线程池的状态是SHUTDOWN,并且workQueue中还存在有任务,此时为了加快处理完队列中的任务,会创建一个新的线程从workqueue中获取任务执行,此时创建的线程是不接收新任务的,只会从workQueue中获取任务,参考源码
ThreadPoolExecutor#addWorker()
方法
-
- 线程把任务丢给线程池后肯定就马上返回了?
要看线程池的拒绝策略,如果拒绝策略使用的是CallerRunsPolicy,那么提交任务的线程在提交任务后并不能保证马上返回,当触发了这个reject策略时就需要调用者亲自执行这个线程。
-
- 线程池里的线程异常后会再次新增线程吗,如何捕获这些线程抛出的异常?
0)会把异常的线程移除掉,并重新创建一个添加到线程池中
当线程异常,会调用ThreadPoolExecutor.runWorker()
方法最后面的finally
中的processWorkerExit()
,会将此线程remove,并重新addworker()一个线程。
1)捕获抛出的异常
- execute提交的任务,需要自己实现ThreadFactory,设置UncaughtExceptionHandler,从中获取到线程异常;(参考52题)
- submit提交的任务,返回结果封装在future中,如果调用future.get()方法则必须进行异常捕获,从而可以抛出(打印)堆栈异常。底层虽然直接捕获了异常,但是没有直接抛出,而是将异常暂时保存了起来,所以此时没有打印堆栈异常,等到调用Future.get()方法时才抛出异常,这么做是为了不影响其他线程的任务执行。
-
- 线程池的大小如何设置,如何动态设置线程池的参数
CPU密集型: Ncpu+1
IO密集型: 2NCPU
动态调整参数:
setCorePoolSize(int corePoolSize) 调整核心线程池大小
setMaximumPoolSize(int maximumPoolSize)
setKeepAliveTime() 设置线程的存活时间
-
- 线程池的状态画一下?
示意图:
- 线程池的状态画一下?
五种状态:
1) RUNNING: 接收新的任务,并继续处理workQueue中的任务;
2)SHUTDOWN: 不再接收新的任务,但是能继续处理 workQueue中的任务;
3)STOP: 不再接收新的任务,也不再处理workQueue中的任务,并且会中断正在执行任务的线程;
4)TIDYING:所有的任务都完结了,并且线程数量(workCount)为 0 时即为此状态,进入此状态后会调用 terminated() 这个钩子方法进入 TERMINATED 状态;
5)TERMINATED:调用 terminated() 方法后即为此状态
-
- 阿里 Java 代码规范为什么不允许使用 Executors 快速创建线程池?
1.newCachedThreadPool()设置最大线程数为Integer.MAX_VALUE;
2.newSingleThreadExecutor中队列使用的是LinkedBlockingQueue,未指定大小,相当于无界队列;
3.以上两者可能会导致OOM;
-
- 使用线程池应该避免哪些问题,能否简单说下线程池的最佳实践?
- 线程池执行的任务应该是互相独立的,如果互相依赖的话,可能导致死锁
- 核心任务与非核心任务最好能用多个线程池隔离开来
- 添加线程池监控,动态设置线程池
-
- 如何优雅关闭线程池
关闭线程池的方法有
shutdown
和shutdownnow
,两者的区别:
- shutdown:
- 调用shutdown方法后,将线程池状态改为SHUTDOWN
- 调用interruptIdleWorkers中断空闲线程,但是正在执行线程池中任务的线程不会被中断,即使线程处于阻塞状态也不会中断,而是继续执行;
- 如果线程阻塞等待从队列里读取任务,则会被唤醒,但是会继续判断队列是否为空,不为空会继续从队列里读取任务,为空则线程退出;
因此使用shutdown方法,必须保证任务里不会有阻塞等待的逻辑,否则线程池就关闭不了;
- shutdownNow:
- 调用shutdownNow方法后,将线程池状态改为STOP;
- 将队列里还没有执行的任务放到列表里,返回给调用方;
- 遍历线程池里的所有工作线程,然后调用线程的interrupt方法;
执行shutdownNow方法时,如果线程因为正在执行提交到线程池中的任务而处于阻塞状态,就会导致报错,因此在使用shutdownNow方法时,一定要对任务里进行异常捕获;综上,可以两者兼并使用,springBoot里边,可以在创建线程池的时候设置如下:
- setWaitForTasksToCompleteOnShutdown(true): 该方法用来设置 线程池关闭 的时候 等待 所有任务都完成后,再继续 销毁 其他的 Bean,这样这些 异步任务 的 销毁 就会先于 数据库连接池对象 的销毁
- setAwaitTerminationSeconds(60): 该方法用来设置线程池中 任务的等待时间,如果超过这个时间还没有销毁就 强制销毁,以确保应用最后能够被关闭,而不是阻塞住
-
- 如何对线程池进行监控
1.开启一个定时线程,定时对线程池指标进行采集,可以采集的指标包含:
- int getCorePoolSize():获取核心线程数。
- int getLargestPoolSize():历史峰值线程数。
- int getMaximumPoolSize():最大线程数(线程池线程容量)。
- int getActiveCount():当前活跃线程数
- int getPoolSize():当前线程池中的线程总数
- BlockingQueuegetQueue() 当前线程池的任务队列,据此可以获取积压任务的总数,getQueue.size()
2.使用开源工具如 Grafana + Prometheus + MicroMeter 来实现
-
- 为什么要使用线程池?
1) java线程模型是基于操作系统的原生线程模型实现的,因此java线程的创建和销毁都会导致系统调用,而系统调用需要在内核态和用户态之间来回切换,代价相对较高;
2) 每个线程都需要一个内核线程的支持,也就是说每个线程都需要消耗一定的内核资源,因此能创建的线程数是有限的;
3) 线程多了,导致不可忽视的上下文切换;
-
- 线程提交任务的方式
- submit
- execute
两者的区别:
- execute无返回值,submit返回Future,使用Future可以取消线程,查询线程是否已取消或者完成,甚至可以阻塞线程;
- execute执行过程中,如果发生了异常是捕获不到的,默认会执行ThreadGroup的uncaughtException方法(下图2部分)
如果想自己监控execute方法的异常,需要通过threadFactory来指定一个UncaughtExceptionHandler, 如下所示:
public static void main(String[] args) {
ThreadFactory factory = (Runnable r)->{
Thread t = new Thread(r);
t.setUncaughtExceptionHandler((Thread t1, Throwable thr)->{
// 在此设置异常监控逻辑
System.out.println("线程"+t1.getName()+"发生异常,异常原因:"+thr.getMessage());
});
return t;
};
ExecutorService service = new ThreadPoolExecutor(1,1,0, TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(10),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//使用自定义的factory,自定义UncaughtExceptionHandler,当线程内部不捕获异常时,使用UncaughtExceptionHandler可以获取到异常信息
ExecutorService service1 = new ThreadPoolExecutor(1,1,0, TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(10),factory,new ThreadPoolExecutor.AbortPolicy());
Runnable runnable = ()-> System.out.println(10/0);
service.execute(runnable);
service1.execute(runnable);
System.out.println("------------------------------------------------------");
Future f = service.submit(runnable);
try {
Object o = f.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
System.out.println("submit时出现异常,异常原因:"+e.getMessage());
} finally {
}
service.shutdown();
service1.shutdown();
}
-
- 如何实现核心线程池的预热?
使用 prestartAllCoreThreads() 方法,这个方法会一次性创建 corePoolSize 个线程,无需等到提交任务时才创建,提交创建好线程的话,一有任务提交过来,这些线程就可以立即处理
-
- synchronized锁优化
JDK1.6对锁的实现进行了大量的优化,包括自旋锁、适应性自旋锁、锁粗化、锁消除、偏向锁、轻量级锁等技术来较少锁操作的开销。
- 自旋锁
- 适应性自旋锁
- 锁粗化
- 锁消除
-
- synchronized锁升级
锁的四种状态
- 无锁状态
- 偏向锁
- 轻量级锁
- 重量级锁
随着竞争的激烈程度增加,锁的状态也在逐渐升级,但是锁的状态只会升级不会出现降级的情况,这种策略是为了提高获取锁和释放锁的效率。
轻量级锁
目的:引入轻量级锁的目的是为了在没有多线程竞争的情况下,减少传统的重量级锁使用操作系统互斥量导致的性能损耗。
产生条件:关闭偏向锁或者偏向锁产生竞争导致升级为轻量级锁。
获取锁的步骤:
- 1.判断当前对象是否处于无锁状态(hashcode、0、01),若是,JVM会首先在当前线程的栈帧中创建一个Lock record的空间,用来存储对象目前的Mark Word的拷贝(官方在这份拷贝前加了一个Displaced,所以叫做Displaced Mark Word),否则执行步骤3;
- 2.JVM利用CAS尝试将对象的Mark Word更新为指向Lock record的指针,如果成功表示竞争到锁,则将锁标志位更新为00(表示此对象处于轻量锁状态),执行同步操作;否则执行步骤3;
- 3.判断当前对象的Mark Word是否指向当前线程的栈帧,如果是表示当前线程已经持有当前对象的锁,直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。
释放锁的步骤:
- 1.取出在获取到轻量级锁时获取到的Displaced Mark Word中的数据;
- 2.用CAS操作将取出的数据替换到当前对象的对象头中的Mark Word中,如果成功则说明释放锁成功,如果失败,执行步骤3;
- 3.如果CAS操作失败,说明有其他线程在尝试获取锁,则需要在释放锁的同时唤醒其他被挂起的线程。
轻量级锁的流程图:
偏向锁
目的:引入偏向锁的目的是为了在没有多线程竞争的情况下,尽量减少不必要的轻量级锁的执行路径。
如何减少轻量级锁的CAS操作?
根据对象头中的Mark Word机构,只需要检查是否为偏向锁、锁标识、以及ThreadId即可。
获取锁步骤:
- 1.检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
- 2.若为可偏向状态,则检测线程ID是否是当前线程ID,如果是,则执行步骤5,否则执行步骤3;
- 3.如果线程ID不是当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行步骤4;
- 4.通过CAS竞争锁失败,说明有其他线程竞争锁,当到达全局安全点时,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
- 5.执行同步代码块;
释放锁步骤:
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点上是有没代码在执行的),具体步骤如下:
- 1.暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态;
- 2.撤销偏向锁,恢复到无锁状态或者轻量级锁状态;
偏向锁流程图:
重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程的切换需要从用户态到内核态的切换,切换成本非常高。