目录
13.synchronized优化的过程和markword息息相关
24.volatile解决指令重排序Hotspot实现(了解即可)
5.ReentrantLock和synchronized有什么不同
8.countDownLatch.countDown()注意事项
2.LockSupport中的park()和unpark()方法的实现原理
五.AQS源码与ThreadLocal源码与强软虚弱4种引用
1.通过AQS是如何设置链表尾巴来理解AQS为什么效率这么高?
6.ConcurrentHashMap为什么读取效率那么快?
3.Callable、Runnable、Future、FutureTask
10.并发和并行的区别concurrent vs parallel
11.线程池中 submit() 和 execute()方法有什么区别?
一.线程的基本概念
1.请告诉我启动线程的三种方式?
你说第一个:new Thread().start;第二个:new Thread(Runnable).start();那第三个呢, 要回答线程池也是用的这两种之一,他这么问有点吹毛求疵的意思,你就可以说通过线程池 也可以启动一个新的线程 3:executors.new acahedThreadPool()或者Future+Callback
2.Thread.yield()方法的理解?
就是当前线程正在执行的时候停止下来进入,等待队列,回到等待队列里在系统的调度算法里头呢还是依然有可能把你刚回去的这个线程拿回来继续执行,当然,更大的可能性是把原来等待的那些拿出来一个来执行,所以yield的意思是我让出一下CPU,后面你们能不能抢到那我不管
注意:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果
3.Thread.join()方法的理解?
join意思是在自己当前线程加入你调用join的线程,本线程等待。等调用的线程运行完了,自己再去执行。
4.线程的状态?
说明:调用yield方法会从Running状态跑到Ready状态,线程调度器选中执行的时候又从Ready状态跑到Running状态去了;在运行的时候如果调用了o.wait()、t.join、LockSupport().park()进入waiting状态,调用o.notify()、o.notifiall、LockSupport.unpark()又回到Running状态;TimeWaiting按照时间等待,等时间结束自己就回去了,Thread.sleep(time)、o.wait(time)、t.join(time)、LockSupport.parkNanos()、LockSupport.ParkUntil()这些都是关于时间等待的方法。
NEW 状态是指线程刚创建, 尚未启动
RUNNABLE 状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等
BLOCKED 这个状态下, 是在多个线程有同步操作的场景, 比如正在等待另一个线程的synchronized 块的执行释放, 或者可重入的 synchronized块里别人调用wait() 方法, 也就是这里是线程在等待进入临界区
WAITING 这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在临界点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束
TIMED_WAITING 这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态
TERMINATED 这个状态下表示 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)
注意:Java中没有ready和running状态,实际上Java的runnable状态已经包含了ready和running,甚至还可能有包括 waiting 状态的部分细分状态。
waiting和blocked区别:与waiting状态相关联的是等待队列,与blocked状态相关的是同步队列,一个线程由等待队列迁移到同步队列时,线程状态将会由waiting转化为blocked.也可以这样说:blocked状态是处于waiting状态的线程重新焕发生命力的必由之路。
5.哪些是JVM管理的?哪些是操作系统管理的?
上面这些状态全是由JVM管理的,因为JVM管理的时候也要通过操作系统,所以呢,哪个是操作系统和哪个是JVM他俩分不开,JVM是跑在操作系统上的一个普通程序。
6.线程什么状态时候会被挂起?挂起是否也是一个状态?
Running的时候,在CPU上会跑很多个线程,CPU会隔一段时间执行这个线程一下,在隔一段时间执行那个线程一下,这是CPU内部的一个调度,把这个状态线程扔出去,从running扔回去就叫线程被挂起,CPU控制它
7.T.class是单例的吗?
一个classload到内存它是不是单例的,想想看。一般情况下是,如果在同一个classload空间它一定是。不是同一个类加载器就不是了,不同的类加载器互相之间也不能访问。所以说你能访问它,那他一定就是单例
8.synchronized的另一个属性:可重入
如果是一个同步方法调用另一个同步方法,有一个方法加了锁,另一个方法也需要加锁,加的是同一把锁也是同一个线程,那这个时候申请仍然会得到该对象的锁。
重入次数必须记录,因为要解锁几次必须得对应
偏向锁 自旋锁 -> 线程栈 -> LR + 1
重量级锁 -> ? ObjectMonitor字段上
9.异常锁
* 程序在执行过程中,如果出现异常,默认情况下锁会释放 * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况 * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适 * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据 * 因为要非常小心的处理同步业务逻辑中的异常
10.偏向锁、自旋锁、轻量级锁、重量级锁
偏向锁:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后,消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。另外,JVM对那种会有多线程加锁,但不存在锁竞争的情况也做了优化,听起来比较拗口,但在现实应用中确实是可能出现这种情况,因为线程之前除了互斥之外也可能发生同步关系,被同步的两个线程(一前一后)对共享对象锁的竞争很可能是没有冲突的。对这种情况,JVM用一个epoch表示一个偏向锁的时间戳(真实地生成一个时间戳代价还是蛮大的,因此这里应当理解为一种类似时间戳的identifier)
自旋锁:线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。同时我们可以发现,很多对象锁的锁定状态只会持续很短的一段时间,例如整数的自加操作,在很短的时间内阻塞并唤醒线程显然不值得,为此引入了自旋锁。
所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会出让时间片,所以其他线程依旧有申请锁和释放锁的机会。
自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。
轻量级锁:
加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。
解锁
轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程。
重量级锁:重量锁在JVM中又叫对象监视器(Monitor),它很像C中的Mutex,除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。
锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程使用自旋会消耗CPU | 追求响应时间,锁占用时间很短 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,锁占用时间较长 |
二.volatile和CAS
1.volatile关键字
* volatile关键字,使一个变量在多个线程间可见 * A B线程都用到一个变量,Java默认是A线程中保留一份copy,这样如果B线程修改了该变量,A线程未必知道 * 使用volatile关键字,会让所有线程都会读到变量的修改值 * 当线程开始运行的时候,会把变量从堆内存中读取到该线程的工作区,在运行的过程中直接使用这个copy,并不会每次都去读取堆内存 * 这样当其他线程修改该变量的值后,当前线程感知不到 * 使用volatile,将会强制所有线程都会去堆内存中读取running的值 * volatile并不能保证多个线程共同修改同一变量时带来的不一致问题,也就说说volatile不能替代synchronize
2.volatile的作用
-
保证线程的可见性
-
MESI
-
缓存一致性协议
volatile跟以上没有关系,但实现方式一致
-
-
禁止指令重排(CPU)
-
DCL单例
-
Double Check Lock(双重检查锁)
-
Mgr06.java
-
loadfence原语指令
-
storefence原语指令
-
-
3.DCL单例
核心:双重检查、volatile关键字
private volatile static Mgr6 INSTANCE;
private Mgr6() {
}
public static Mgr6 getInstance() {
if (INSTANCE == null) {
synchronized (Mgr6.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (INSTANCE == null) {
INSTANCE = new Mgr6();
}
}
}
return INSTANCE;
}
4.什么是指令重排
Java语言规范JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
指令重排序的意义:使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。
as-if-serial语义的意思是:不管怎么进行指令重排序,单线程内程序的执行结果不能被改变。编译器,处理器进行指令重排序都必须要遵守as-if-serial语义规则。
多线程中指令重新排序可能会改变程序的执行结果。
重排序满足happen before原则
-
程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作
-
管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序,下同)对同一个锁的lock操作
-
volatile变量规则:对一个volatile变量的写操作happen—before后面对该变量的读操作
-
线程启动规则:Thread对象的start()方法happen—before此线程的每一个动作
-
线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行
-
线程中断规则:对线程interrupt()方法的调用happen—before发生于被中断线程的代码检测到中断时事件的发生
-
对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始
-
传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C
5.synchronize总结
synchronize锁的是对象而不是代码,锁方法锁的是this,锁static方法锁的是class,锁定方法和非锁定方法是可以同时执行的,锁升级从偏向锁到自旋锁到重量级锁
6.volatile总结
volatile保证线程的可见性,同时防止指令重新排序。线程可见性在CPU的级别是用缓存一致性来保证的;禁止指令重排序CPU级别是你禁止不了的,那是人家内部运行的过程,提高效率的。但是在虚拟机级别你加了volatile之后,这个指令重新排序就可以禁止。严格来讲,还要去深究它的内部的话,它是加了读屏障和写屏障,这是CPU的一个原语。
7.什么是CAS
CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换;
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
8.什么是ABA问题?
如线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,并且线程2进行了一些操作将内存X中的值变成了B,然后线程2又将内存X中的数据变成A,这时候线程1进行CAS操作发现内存X中仍然是A,然后线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化
9.如何解决ABA问题
如果是基本数据类型:无所谓,不影响结果
如果是引用数据类型:加版本号,做任何一个值的修改,修改完后加一,后面检查的时候连带版本号一起检查
10.CAS的缺点
-
CPU开销过大
-
不能保证代码块的原子性
-
ABA问题
乱序 dcl中可以不加外面那层锁吗 内存屏障 happens-before
缓存行的伪共享
11.为什么有自旋锁还需要重量级锁?
自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗
重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源
12.偏向锁是否一定比自旋锁效率高?
不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁
JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开
13.synchronized优化的过程和markword息息相关
用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位
14.默认情况 偏向锁有个时延,默认是4秒 why?
因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
JVM参数:-XX:BiasedLockingStartupDelay=0 设置延迟时间
15.偏向锁
如果有线程上锁 上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程 偏向锁不可重偏向 批量偏向 批量撤销
16.偏向锁升级到轻量级锁
有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁
17.轻量级锁升级到重量级锁
自旋超过10次,升级为重量级锁 - 如果太多线程自旋 CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)
JVM参数:-XX:PreBlockSpin 设置自旋次数
18.适应性自旋锁
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。
19.synchronized最底层实现(可忽略)
汇编码,会看到 lock comxchg .....指令
20.synchronized vs Lock (CAS)
在高争用 高耗时的环境下synchronized效率更高 在低争用 低耗时的环境下CAS效率更高 synchronized到重量级之后是等待队列(不消耗CPU) CAS(等待期间消耗CPU) 一切以实测为准
21.锁消除 lock eliminate
public void add(String str1,String str2){
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。
22.锁粗化 lock coarsening
public String test(String str){
int i = 0;
StringBuffer sb = new StringBuffer():
while(i < 100){
sb.append(str);
i++;
}
return sb.toString():
}
JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 循环体外),使得这一连串操作只需要加一次锁即可。
23.CPU的基础知识
缓存行对齐 缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高
24.volatile解决指令重排序Hotspot实现(了解即可)
bytecodeinterpreter.cpp
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}
orderaccess_linux_x86.inline.hpp
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
LOCK 用于在多处理器中执行指令时对共享内存的独占使用。 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。 另外还提供了有序的指令无法越过这个内存屏障的作用。
三.Atomic类和线程同步新机制
1.atomic类
atomic内部用了cas操作,无锁操作,是解决同样的问题最高效的方法,atomicxxx类本身方法都是原子性的但不能保证多个方法连续调用是原子性的
2.为什么atomic要比sync快?
因为不加锁,synchronize是要加锁的,有可能它要去操作系统申请重量级锁,所以synchronize效率偏低,在这种情形下效率偏低。
3.LongAdder为什么要比Atomic效率要高呢?
因为LongAdder的内部做了一个分段锁,类似于分段锁的概念。在它内部的时候,会把一个值放到一个数组里,比如说数组长度是4,最开始是0,1000个线程,250个线程锁在第一个数组元素里,以此类推,每一个都是往上递增算出来结果再加在一起。
4.可重入锁Reentrantlock
使用ReentrantLock可以完成和synchronized一样的功能,内部实现CAS修改state,AQS独占模式,但是需要注意的是,必须要手动释放锁,使用sync锁定的话如果遇到异常,jvm会手动释放锁。ReentrantLock手动解锁一定要写在try...finally里面保证最好 一定要解锁,不然的话上锁之后中间执行的过程就有问题了,死在那里了,别人永远也拿不到这把锁了。
5.ReentrantLock和synchronized有什么不同
-
ReentrantLock有一些功能还是要比sync强大,你可以使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行,sync如果搞不定的话他肯定就阻塞了,但是用ReentrantLock你自己就可以解决你到底要不要wait
-
ReentrantLock用另外一种方式加锁lock.lockInterruptibly,可以通过thread.intrerrupt打断。
-
ReentrantLock还可以指定为公平锁,true表示公平锁
private static ReentrantLock lock = new ReentrantLock(true);
公平锁的意思是谁等再前面就先执行,当前线程会先检查队列里有没有原来等着的,如果有他就先进队列里等着别人先运行。默认是非公平锁
6.CountDownLatch
CountDown叫倒数,latch是门栓的意思(倒数的一个门栓,5、4、3、2、1数到了这个门栓就开了)。
CountDownLatch是具有synchronized机制的一个工具,目的是让一个或者多个线程等待,直到其他线程的一系列操作完成。与thread.join功能类似
7.CountDownLatch和Join用法的区别?
在使用join()中,多个线程只有在执行完毕之后才能被解除阻塞,而在CountDownLatch中,线程可以在任何时候任何位置调用countdown方法减少计数,通过这种方式,我们可以更好地控制线程的解除阻塞,而不是仅仅依赖于连接线程的完成。
8.countDownLatch.countDown()注意事项
这一句话尽量写在finally中,或是保证此行代码前的逻辑正常运行,因为在一些情况下,出现异常会导致无法减一,然后出现死锁。
CountDownLatch 是一次性使用的,当计数值在构造函数中初始化后,就不能再对其设置任何值,当 CountDownLatch 使用完毕,也不能再次被使用。
9.什么是AQS
队列同步器,AQS定义了一套多线程访问共享资源的同步器框架,这么好用的功能是怎么实现的呢,下面就来说一说实现它的核心技术原理 AQS。 AQS 全称 AbstractQueuedSynchronizer
,是 java.util.concurrent 中提供的一种高效且可扩展的同步机制。它可以用来实现可以依赖 int 状态的同步器,获取和释放参数以及一个内部FIFO等待队列,除了CountDownLatch
,ReentrantLock
、Semaphore
等功能实现都使用了它。
10.countDownLatch原理
CAS修改AbstractQueuedSynchronized中的state,AQS共享模式,无限循环获取state
11.FIFO队列
AQS的实现依赖内部的同步队列(FIFO双向队列),如果当前线程获取同步状态失败,AQS会将该线程以及等待状态等信息构造成一个Node,将其加入同步队列的尾部,同时阻塞当前线程,当同步状态释放时,唤醒队列的头节点。
12.CyclicBarrier 是什么
从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。
它的作用就是会让所有线程都等待完成后才会继续下一步行动。
CyclicBarrier cyclicBarrier = new CyclicBarrier(20, () -> {
System.out.println("满人");
System.out.println(Thread.currentThread().getName());
});
13.CyclicBarrier 使用场景
可以用于多线程计算数据,最后合并计算结果的场景。
14.Phaser(了解)
它与CountDownLatch非常相似,允许我们协调线程的执行。与CountDownLatch相比,它具有一些额外的功能.
Phaser是在线程动态数需要继续执行之前等待的屏障。在CountDownLatch中,该数字无法动态配置,需要在创建实例时提供。
其功能跟CyclicBarrier和CountDownLatch有些重叠,但是提供了更灵活的用法,例如支持动态调整注册任务的数量等。本文在Phaser自带的示例代码基础上进行一下简单的分析。
15.ReentrantReadWriteLock
-
可重入
如果你了解过synchronized关键字,一定知道他的可重入性,可重入就是同一个线程可以重复加锁,每次加锁的时候count值加1,每次释放锁的时候count减1,直到count为0,其他的线程才可以再次获取。
-
读写分离
我们知道,对于一个数据,不管是几个线程同时读都不会出现任何问题,但是写就不一样了,几个线程对同一个数据进行更改就可能会出现数据不一致的问题,因此想出了一个方法就是对数据加锁,这时候出现了一个问题:
线程写数据的时候加锁是为了确保数据的准确性,但是线程读数据的时候再加锁就会大大降低效率,这时候怎么办呢?那就对写数据和读数据分开,加上两把不同的锁,不仅保证了正确性,还能提高效率。
-
可以锁降级
线程获取写入锁后可以获取读取锁,然后释放写入锁,这样就从写入锁变成了读取锁,从而实现锁降级的特性。
-
不可锁升级
线程获取读锁是不能直接升级为写入锁的。需要释放所有读取锁,才可获取写锁,
16.Semaphore
信号量 Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。就这一点而言,单纯的synchronized 关键字是实现不了的。
17.Exchanger
Exchanger是java 5引入的并发类,Exchanger顾名思义就是用来做交换的。这里主要是两个线程之间交换持有的对象。当Exchanger在一个线程中调用exchange方法之后,会等待另外的线程调用同样的exchange方法。
两个线程都调用exchange方法之后,传入的参数就会交换。
四.LockSupport与源码阅读方法
1.LockSupport
LockSupport
是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。
-
LockSupport不需要synchronized加锁就可以实现线程的阻塞和唤醒
-
LockSupport.unpark()可以先于LockSupport.park()执行,并且线程不会阻塞
-
如果一个线程处于等待状态,连续调用两次park()方法,就会使该线程永远无法被唤醒
2.LockSupport中的park()和unpark()方法的实现原理
park()和unpark()方法的实现是有Unsafe类提供的,而Unsafe类是由C和C++语言完成的,其实原理也是比较好理解的,它主要通过一个变量作为一个标识,变量在0,1之间来回切换,当这个变量大于0的时候线程就获得了”令牌“,从这一点我们不难知道,其实park()和unpark()方法就是在改变这个变量的值,来达到线程的阻塞和唤醒的。
3.volatile修饰引用类型
volatile一定要尽量去修饰普通的值,不要去修饰引用值,因为volatile修饰引用类型,这个引用对象指向的是另外一个new出来的对象,如果这个对象里边的成员变量的值改了,是无法观察到的
4.object类中wait()和notify()
wait()等待并释放锁;notify()唤醒但是不释放锁
5.阅读源码的原则
-
跑不起来的不读,解决问题就好、一条线索到底、无关细节略过
-
画两种图:第一种是方法之间的调用图,第二种类图
五.AQS源码与ThreadLocal源码与强软虚弱4种引用
1.通过AQS是如何设置链表尾巴来理解AQS为什么效率这么高?
如果用sync锁整个表锁的太多太大了,现在只观测tail这一个节点就可以了,通过CAS操作修改tail
2.为什么是双向链表?
添加一个线程节点的时候,需要看一下前面节点的状态,如果前面的节点是持有线程的过程中,这时候必须在后面排队,如果前面节点取消,就得越过这个节点,所以在需要考虑前面节点的时候,就必须是双向
3.ThreadLocal
ThreadLocal里的对象不用了,务必要remove掉,不然会有内存泄露
ThreadLocal t1 = new ThreadLocal();
t1.set(new M());
t1.remove();
4.强软虚弱引用
-
强引用:NormalReference,只有有一个应用指向这个对象,那么垃圾回收器一定不会回收它,这就是普通引用,也就是强引用;为什么不会回收?因为有引用指向,所以不会回收,只有没有引用指向的时候才会回收。
-
软引用:SoftReference,当有一个对象(字节数组)被一个软引用所指向的时候,只有系统内存不够用的时候,才会回收它(字节数组)。
使用场景:做缓存 1.缓存大图片 2.缓存数据库里的大数据
-
弱引用:WeakReference,只要遭遇GC就会回收。
ThreadLocal中的有用到;spring中DefaultListableBeanFactory类setSerializationId方法有用到:serializableFactories.put(serializationId, new WeakReference(this));
-
虚引用:PhantomReference,对一个对象而言,这个引用形同虚设,有和没有一样,当试图通过虚引用的get()方法取得强引用时,总是会返回null,并且,虚引用必须和引用队列一起使用。
它的作用在于跟踪垃圾回收过程,在对象被收集器回收时收到一个系统通知。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会彻底销毁该对象。 所以可以通过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。
如果一个对象没有强引用和软引用,对于垃圾回收器而言便是可以被清除的,在清除之前,会调用其finalize方法,如果一个对象已经被调用过finalize方法但是还没有被释放,它就变成了一个虚可达对象。
5.虚引用使用场景
使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。这个虚引用对于对象而言完全是无感知的,有没有完全一样,但是对于虚引用的使用者而言,就像是待观察的对象的把脉线,可以通过它来观察对象是否已经被回收,从而进行相应的处理。
事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。
六.并发容器
1.容器分类
2.Hashtable
自带锁,效率低下,基本不用。
3.HashMap
适用于单线程,多线程下会产生死循环,最终CPU占用可能到100%。
4.SynchronizedHashMap
用的是synchronizeMap,给HashMap手动加锁,它的源码自己做了一个Object,严格来说他和Hashtable效率上区别不大。
5.ConcurrentHashMap
插入数据的时候内部又做了各种各样的判断,本来是链表,到了8之后又变成了红黑树,然后里面又做了各种各样的cas判断,所以插入数据效率是要更低一些。
对比:hashMap和HashTable读效率低,插入效率高。ConcurrentHashMap的效率主要体现在读上,多线程基本用ConcurrentHashMap。
6.ConcurrentHashMap为什么读取效率那么快?
-
使用CAS乐观锁和volatile代替RentrantLock
-
spread二次哈希进行segment分段(JDK1.7)JDK1.8锁的是桶的位置
-
stream提高并行处理能力
7.CAS一定比synchronized效率高吗?
不一定,要看并发量的高低,要看你锁定之后代码执行的时间,任何时候再你实际情况下都需要通过测试,压测来决定用哪种容器。
8.ConcurrentSkipListMap
ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景。使用
跳表的数据结构,以空间换时间,提高存取速度。
跳表概念简述:
跳表(SkipList):增加了向前指针的链表叫做指针。跳表全称叫做跳跃表,简称跳表。跳表是一个*随机化*的数据结构,实质是一种可以进行二分查找的有序链表。跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能
跳表插入:抛硬币方式决定是否向上一层
跳表删除:如某一层只有一个节点,则删除该层
跳表查询:从最上面一层开始查询
9.CopyOnWriteArrayList
读的时候不加锁,写的时候加锁,写的时候在原来的基础上拷贝一个,拷贝的时候扩展出一个新的元素来,然后把新添加的元素添加到最后面的位置上,与此同时把只想老的容器的一个引用指向新的,这个写法就是写时复制。
写的效率比较低,因为每次写都要复制,在读多写少的情况下使用。
10.ConcurrentLinkedQueue
ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全的队列,按照先进先出原则对元素进行排序。新元素从队列尾部插入,而获取队列元素,则需要从队列头部获取。cas+volatile操作head、tail保证线程安全。
-
入列出列线程安全,遍历不安全
-
不允许添加null元素
-
底层使用列表与cas算法包装入列出列安全
11.LinkedBlockingQueue
阻塞队列,无边界的,LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行。LinkedBlockingQueue采用可重入锁(ReentrantLock)来保证在并发情况下的线程安全。
12.LinkedBlockingQueue常用操作
-
取数据
take():首选。当队列为空时阻塞
poll():弹出队顶元素,队列为空时,返回空
peek():和poll类似,返回队队顶元素,但顶元素不弹出。队列为空时返回null
remove(Object o):移除某个元素,队列为空时抛出异常。成功移除返回true
-
添加数据
put():首选。队满是阻塞
offer():队满时返回false
-
判断队列是否为空
size()方法会遍历整个队列,时间复杂度为O(n),所以最好选用isEmpty
13.ArrayBlockingQueue
ArrayBlockingQueue
是一个线程安全的、基于数组、有界的、阻塞的、FIFO 队列。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。ArrayBlockingQueue读写共享一把锁
14.BlockingQueue接口中对应的方法说明
操作 | 抛出异常 | 返回特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
Insert | add(e) | offer(e) | put(e) | offer(e, time, unit) |
Remove | remove() | poll() | take() | poll(time, unit) |
Examine | element() | peek() | 无 | 无 |
BlockingQueue虽然比起Queue在操作上提供了更多的支持,但是它在使用的使用也应该如下的几点:
-
BlockingQueue中是不允许添加null的,该接受在声明的时候就要求所有的实现类在接收到一个null的时候,都应该抛出NullPointerException。
-
BlockingQueue是线程安全的,因此它的所有和队列相关的方法都具有原子性。但是对于那么从Collection接口中继承而来的批量操作方法,比如addAll(Collection e)等方法,BlockingQueue的实现通常没有保证其具有原子性,因此我们在使用的BlockingQueue,应该尽可能地不去使用这些方法。
-
BlockingQueue主要应用于生产者与消费者的模型中,其元素的添加和获取都是极具规律性的。但是对于remove(Object o)这样的方法,虽然BlockingQueue可以保证元素正确的删除,但是这样的操作会非常响应性能,因此我们在没有特殊的情况下,也应该避免使用这类方法。
15.Queue接口中对应的方法说明
方法说明:
操作 | 抛出异常 | 返回特殊值 |
---|---|---|
Insert | add(e) | offer(e) |
Remove | remove() | poll() |
Examine | element() | peek() |
-
add方法在将一个元素插入到队列的尾部时,如果出现队列已经满了,那么就会抛出IllegalStateException,而使用offer方法时,如果队列满了,则添加失败,返回false,但并不会引发异常。
-
remove方法是获取队列的头部元素并且删除,如果当队列为空时,那么就会抛出NoSuchElementException。而poll在队列为空时,则返回一个null。
-
element方法是从队列中获取到队列的第一个元素,但不会删除,但是如果队列为空时,那么它就会抛出NoSuchElementException。peek方法与之类似,只是不会抛出异常,而是返回false。
16.DelayQueue
-
DelayQueue是一个无界阻塞队列,只有在延迟期满时,才能从中提取元素。
-
队列的头部,是延迟期满后保存时间最长的delay元素。
使用条件:
存放DelayQueue的元素,必须继承Delay接口,Delay接口使对象成为延迟对象。
该接口强制实现两个方法:
-
CompareTo(Delayed o):用于比较延时,队列里元素的排序依据,这个是Comparable接口的方法,因为Delay实现了Comparable接口,所以需要实现。
-
getDelay(TimeUnit unit):这个接口返回到激活日期的--剩余时间,时间单位由单位参数指定。
此队列不允许使用null元素。
17.DelayQueue使用场景
-
场景一:在订单系统中,一个用户某个时刻下单之后通常有30分钟的时间进行支付,如果30分钟之内没有支付成功,那么这个订单将自动进行过期处理。
-
场景二:用户某个时刻通过手机远程遥控家里的智能设备在指定的时间进行工作。这时就可以将用户指令发送到延时队列,当指令设定的时间到了再将指令推送到只能设备。
-
缓存系统设计:使用DelayQueue保存缓存元素的有效期,用一个线程循环查询DelayQueue,一旦从DelayQueue中取出元素,就表示有元素到期。
-
定时任务调度:使用DelayQueue保存当天要执行的任务和执行的时间,一旦从DelayQueue中获取到任务,就开始执行,比如Timer,就是基于DelayQueue实现的。
18.PriorityQueue
-
PriorityQueue是一种无界的,线程不安全的队列
-
PriorityQueue是一种通过数组实现的,并拥有优先级的队列
-
PriorityQueue存储的元素要求必须是可比较的对象, 如果不是就必须明确指定比较器
19.SynchronousQueue
SynchronousQueue
是一个内部只能包含一个元素的队列。插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。
synchronousQueue在线程池里用处特别大,很多线程取任务,互相之间进行任务的调度都是用它。
20.LinkedTransferQueue
LinkedTransferQueue
是 SynchronousQueue
和 LinkedBlockingQueue
的合体,性能比 LinkedBlockingQueue
更高(没有锁操作,使用LockSupport阻塞和唤醒,CAS+自旋实现),比 SynchronousQueue
能存储更多的元素。
当 put
时,如果有等待的线程,就直接将元素 “交给” 等待者, 否则直接进入队列。
put
和 transfer
方法的区别是,put 是立即返回的, transfer 是阻塞等待消费者拿到数据才返回。transfer
方法和 SynchronousQueue
的 put 方法类似。
七.线程思考题
有两个线程,第一个线程是从1到26,第二个线程是从A到Z,然后要让这两个线程做到同时运行,交替输出,顺序打印。
-
方法一:使用LockSupport.unpark(t2) LockSupport.park();
注意:park()和unpark()方法不一定要顺序执行,原理 LockSupport中的park()和unpark()方法的实现原理
-
方法二:使用synchronized、notify、wait
注意:线程结束时候一定要notify,因为这两个线程里终归有一个线程是wait的,是阻塞停止不动的。如果要保证某个线程先打印,可以用CountDownLatch或者volatile修饰的boolean变量判断
-
方法三:使用lock中的condition,condition.await()和condition.signal()实现,本质上与方法二中的synchronized一样
-
方法四:与方法三相同,不同的是创建两个condition分别对应两个线程。
-
方法五:使用volatile修饰的枚举类+自旋,通过比较当前当前枚举的值实现,这种方式可以定义两个执行的先后顺序
-
方法六:使用ArrayBlockingQueue的take和put方法,队列长度设置为1,利用take没取到数据会阻塞的特性。
-
方法七:建立两个input和两个output利用PipedInputStream和PipedOuputStream的read和write方法,与read空缓冲区会阻塞的特性,实现相互发消息
-
方法八:使用LinkedTransferQueue中的transfer方法和take方法,利用transfer和take的阻塞特性
总结:解题思路应该从阻塞和唤醒这两个角度出发
思考为什么Semaphore和Exchanger解决不了这个问题?
八.线程池
1.Executor
只有一个方法
void execute(Runnable command);
2.ExecutorService
继承了Executor,还完善了整个任务执行器的生命周期。
主要方法:
void shutdown();//结束 List<Runnable> shutdownNow();//马上结束 boolean isShutdown();//是否结束 boolean isTerminated();//是不是整体都执行完了 boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;//等着结束,等多长时间,时间到了还不结束的话就返回false <T> Future<T> submit(Callable<T> task);
3.Callable、Runnable、Future、FutureTask
Callable类似于Runnable,但是有返回值;了解Future,是用来存储执行将来才会产生的结果;FutureTask,他是Future加上Runnable,既可以执行又可以存结果。
4.CompletableFuture
-
runAsync 和 supplyAsync方法
CompletableFuture 提供了四个静态方法来创建一个异步操作。
public static CompletableFuture<Void> runAsync(Runnable runnable) public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
没有指定Executor的方法会使用ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。
runAsync方法不支持返回值。
supplyAsync可以支持返回值。
-
计算结果完成时的回调方法
当CompletableFuture的计算结果完成,或者抛出异常的时候,可以执行特定的Action。主要是下面的方法:
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action) public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action) public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor) public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
可以看到Action的类型是BiConsumer<? super T,? super Throwable>它可以处理正常的计算结果,或者异常情况。
whenComplete 和 whenCompleteAsync 的区别: whenComplete:是执行当前任务的线程执行继续执行 whenComplete 的任务。 whenCompleteAsync:是执行把 whenCompleteAsync 这个任务继续提交给线程池来进行执行。
-
thenApply方法
当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn) public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
Function<? super T,? extends U> T:上一个任务返回结果的类型 U:当前任务的返回值类型
-
handle方法
handle 是执行任务完成时对结果的处理。 handle 方法和 thenApply 方法处理方式基本一样。不同的是 handle 是在任务完成后再执行,还可以处理异常的任务。thenApply 只可以执行正常的任务,任务出现异常则不执行 thenApply 方法。
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn); public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn); public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);
-
thenAccept方法
接收任务的处理结果,并消费处理,无返回结果。
public CompletionStage<Void> thenAccept(Consumer<? super T> action); public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action); public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);
-
thenRun方法
跟 thenAccept 方法不一样的是,不关心任务的处理结果。只要上面的任务执行完成,就开始执行 thenAccept 。
public CompletionStage<Void> thenRun(Runnable action); public CompletionStage<Void> thenRunAsync(Runnable action); public CompletionStage<Void> thenRunAsync(Runnable action,Executor executor);
-
thenCombine
thenCombine 会把 两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。
public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn); public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn); public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);
-
thenAcceptBoth
当两个CompletionStage都执行完成后,把结果一块交给thenAcceptBoth来进行消耗
public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action); public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action); public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action, Executor executor);
-
applyToEither
两个CompletionStage,谁执行返回的结果快,我就用那个CompletionStage的结果进行下一步的转化操作。
public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T> other,Function<? super T, U> fn); public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn); public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn,Executor executor);
-
acceptEither
两个CompletionStage,谁执行返回的结果快,我就用那个CompletionStage的结果进行下一步的消耗操作。
public CompletionStage<Void> acceptEither(CompletionStage<? extends T> other,Consumer<? super T> action); public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action); public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action,Executor executor);
-
runAfterEither
两个CompletionStage,任何一个完成了都会执行下一步的操作(Runnable)
public CompletionStage<Void> runAfterEither(CompletionStage<?> other,Runnable action); public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action); public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor);
-
runAfterBoth
两个CompletionStage,都完成了计算才会执行下一步的操作(Runnable)
public CompletionStage<Void> runAfterBoth(CompletionStage<?> other,Runnable action); public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action); public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor executor);
-
thenCompose
thenCompose 方法允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。
public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn); public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) ; public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) ;
5.ThreadPoolExecutor
-
七个参数:
-
corePoolSize核心线程数
-
maximumPoolSize最大线程数
-
keepAliveTime生存时间
-
TimeUnit生存时间单位
-
BlockingQueue任务队列
-
ThreadFactory线程工厂
-
RejectStrategy拒绝策略->常见的四个(Abort抛异常、Discard扔掉、不抛异常、DiscardOlderest扔掉排队时间最久的、CallerRuns调用者处理服务)
-
6.newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
7.newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程存活时间60秒,只要任务足够多,可无限启动线程。
8.newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
9.newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
10.并发和并行的区别concurrent vs parallel
-
并发是指任务提交,并行是指任务执行,并行是并发的子集
-
并行是多个CPU可以同时进行处理,并发是多个任务同时过来
11.线程池中 submit() 和 execute()方法有什么区别?
-
execute() 参数 Runnable ;submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable)
-
execute() 没有返回值;而 submit() 有返回值
-
submit() 的返回值 Future 调用get方法时,可以捕获处理异常
12.核心线程数计算公式
CPU数 * CPU利用率 * (1+W/C)
W/C:等待时间与计算时间的比率
13.常用变量解释
RUNNING:正常运行的
SHUTDOWN:调用了shutdown方法进入了shutdown状态
STOP:调用了shutdownnow马上让它停止
TIDYING:调用了shutdown然后这个线程也执行完了,现在正在整理的这个过程叫TIDYING
TERMINATED:整个线程全部结束
//可以看做是int类型数字,高3位表示线程池状态,低29位表示worker数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//COUNT_BITS为29位
private static final int COUNT_BITS = Integer.SIZE - 3;
//线程池允许的最大线程数。2^29-1
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//线程池5种状态:按大到小排序如下:RUNNING < SHUTDOWN < STOP < TIDYING <TERMINATED
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
//获取线程池状态,通过按位与操作,低29位将全部变成0
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
//获取线程池worker数量,通过按位与操作,高3位将全部变成0
private static int workerCountOf(int c) { return c & CAPACITY; }
//根据线程池状态和线程池worker数量,生成ctl值
private static int ctlOf(int rs, int wc) { return rs | wc; }
/*
* Bit field accessors that don't require unpacking ctl.
* These depend on the bit layout and on workerCount being never negative.
*/
//线程池状态小于
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
//线程池状态大于等于
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
14.execute()方法
-
第一步:核心线程数不够,启动核心线程
-
第二步:核心线程够了加入队列
-
第三步:核心线程和队列这两个都满了,非核心线程
15.addWorker()方法
-
第一:count+1
-
第二:才是真正加进任务并start
16.WorkStealingPool
工作窃取线程池,实际上是使用ForkJoinPool,默认核心线程数为cpu核数
newWorkStealingPool适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中。
九.JMH与Disruptor
1.JMH
JMH 是 Java Microbenchmark Harness 的缩写。中文意思大致是 “JAVA 微基准测试套件”。
2.Disruptor
-
模式
1.发布订阅模式,同一事件会被多个消费者并行消费 2.点对点模式,同一事件会被一组消费者其中之一消费 3.顺序消费;
-
使用场景
低延迟,高吞吐量,有界的缓存队列
提高吞吐量,减少并发执行上下文之间的延迟并确保可预测延迟
-
为什么RingBuffer这么快?
1.首先是CPU false sharing的解决,Disruptor通过将基本对象填充冗余基本类型变量来填充满整个缓存行,减少false sharing的概率,这部分没怎么看懂,Disruptor通过填充失效这个效果。 (就是一个缓存行8个变量,预设7个变量,然后再保存一个唯一变量,这样就不会出现相同的变量)
2.无锁队列的实现,对于传统并发队列,至少要维护两个指针,一个头指针和一个尾指针。在并发访问修改时,头指针和尾指针的维护不可避免的应用了锁。Disruptor由于是环状队列,对于Producer而言只有头指针而且锁是乐观锁,在标准Disruptor应用中,只有一个生产者,避免了头指针锁的争用。所以我们可以理解Disruptor为无锁队列。
-
为什么要用Disruptor?
锁的成本: 传统阻塞队列使用锁保证线程安全。而锁通过操作系统内核的上下文切换实现,会暂停线程去等待锁直到释放。执行这样的上下文切换,会丢失之前保存的数据和指令。由于消费者和生产者之间的速度差异,队列总是接近满或者空的状态。这种状态会导致高水平的写入争用。 伪共享问题导致的性能低下。 队列是垃圾的重要来源,队列中的元素和用于存储元素的节点对象需要进行频繁的重新分配。
-
生产者线程模式
ProducerType有两种模式Producer.MULTI和Producer.SINGLE,默认是MULTI,如果是确认是单线程,可以指定SINGLE,效率会提升;如果是多个生产者,但模式指定为SINGLE,会出问题
-
等待策略
-
BlockingWaitStrategy:通过线程阻塞的方式,等待生产者唤醒,被唤醒后,再循环检查依赖的sequence是否已经消费。
-
BusySpinWaitStrategy:线程一直自旋等待,可能比较耗cpu
-
LiteBlockingWaitStrategy:线程阻塞等待生产者唤醒,与BlockingWaitStrategy相比,区别在signalNeeded.getAndSet,如果两个线程同时访问一个访问waitfor,一个访问signalAll时,可以减少lock加锁次数.
-
LiteTimeoutBlockingWaitStrategy:与LiteBlockingWaitStrategy相比,设置了阻塞时间,超过时间后抛异常。
-
PhasedBackoffWaitStrategy:根据时间参数和传入的等待策略来决定使用哪种等待策略
-
TimeoutBlockingWaitStrategy:相对于BlockingWaitStrategy来说,设置了等待时间,超过后抛异常
-
YieldingWaitStrategy:尝试100次,然后Thread.yield()让出cpu
-
SleepingWaitStrategy:sleep
-
3.为什么Disruptor的速度这么快?
Disruptor没有使用很影响性能锁 。取而代之的是,在需要确保操作是线程安全的(特别在多生产者的环境下,更新下一个可用的序列号)地方,我们使用CAS(Compare And Swap/Set)操作。这是一个CPU级别的指令,它的工作方式有点像乐观锁——CPU去更新一个值,但如果想改的值不是原来的值,操作就失败,反之则去更新这个值。
说句题外话,Java中AtomicInteger也使用了CAS操作来保证原子性。在并发控制中,CAS操作是十分重要的。
CAS操作是CPU级别的指令,在Java中CAS操作在Unsafe类中(Unsafe,见名知意,这个类是不安全的,不建议在实际开发的时候使用)。关于CAS操作的原理网上有很多,在此不过多说明了。
另一个重要的因素是Disruptor消除了伪共享。 下面引用网上的一段话,来解释下什么是伪共享。
缓存系统中是以缓存行(cache line)为单位存储的。缓存行是 2 的整数幂个连续字节,一般为 32-256 个字节。最常见的缓存行大小是 64个字节。
当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。缓存行上的写竞争是运行在 SMP 系统中并行线程实现可伸缩性最重要的限制因素。有人将伪共享描述成无声的性能杀手,因为从代码中很难看清楚是否会出现伪共享。
第三个原因是Disruptor采用了RingBuffer。Ring Buffer 是一个数组,它比链表要快,而且有一个容易预测的访问模式,数据可以在硬件层面预加载到高速缓存,极大的提高了数据访问的速度。
RingBuffer可以预先分配内存,并且保持数组元素永远有效。这意味着内存垃圾收集(GC)在这种情况下几乎什么也不用做。