目录
1、HashMap如何扩容?
2、高频并发问题
3、synchronized关键字的底层原理是什么?
4、你对CAS的理解以及其底层实现原理可以吗?
4.1、synchronized
4.2、CAS
5、ConcurrentHashMap实现线程安全的底层原理到底是什么?
实现1
实现2
总结:
6、你对JDK中的AQS理解吗?AQS的实现原理是什么?
公平锁和非公平锁
总结
7、线程池
7.1、说说线程池的底层⼯作原理可以吗?
7.2、线程池的核⼼配置参数都是⼲什么的?平时我们应该怎么⽤?
7.3、如果在线程池中使⽤⽆界阻塞队列会发⽣什么问题?
7.4、如果线程池的队列满了之后,会发⽣什么事情吗?
7.5、如果线上机器突然宕机,线程池的阻塞队列中的请求怎么办?
总结
8、谈谈你对Java内存模型的理解可以吗?
9、你知道Java内存模型中的原⼦性、有序性、可⻅性是什么?
9.1、可⻅性
9.2、原⼦性
9.3、有序性
10、能从Java底层⾓度聊聊volatile关键字的原理吗?
11、你知道指令重排以及happens-before原则是什么吗?
12、volatile底层是如何基于内存屏障保证可⻅性和有序性的?
1、HashMap如何扩容?
底层是⼀个数组,当这个数组满了之后,他就会⾃动进⾏扩容,变成⼀个更⼤的数组,让你在⾥⾯可以去放更多的元素
2 倍扩容 [16 位的数组, <> -> <> -> <>] [32 位的数组, <> -> <>, <>] 数组长度 =16 n - 1 0000 0000 0000 0000 0000 0000 0000 1111 hash1 1111 1111 1111 1111 0000 1111 0000 0101 & 结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5 ( index = 5 的位置) n - 1 0000 0000 0000 0000 0000 0000 0000 1111 hash2 1111 1111 1111 1111 0000 1111 0001 0101 & 结果 0000 0000 0000 0000 0000 0000 000 0 0101 = 5 ( index = 5 的位置) 在数组长度为 16 的时候,他们两个 hash 值的位置是⼀样的,⽤链表来处理,出现⼀个 hash 冲突的问题 如果数组的长度扩容之后 = 32 ,重新对每个 hash 值进⾏寻址,也就是⽤每个 hash 值跟新数组的 length - 1 进⾏与操作 n-1 0000 0000 0000 0000 0000 0000 000 1 1111 hash1 1111 1111 1111 1111 0000 1111 0000 0101 & 结果 0000 0000 0000 0000 0000 0000 0000 0101 = 5 ( index = 5 的位置) n-1 0000 0000 0000 0000 0000 0000 000 1 1111 hash2 1111 1111 1111 1111 0000 1111 0001 0101 & 结果 0000 0000 0000 0000 0000 0000 000 1 0101 = 21 ( index = 21 的位置) 判断⼆进制结果中是否多出⼀个bit的1,如果没多,那么就是原来的index,如果多了出来,那么就是index + oldCap,通过这个⽅式, 就避免了rehash 的时候,⽤每个 hash 对新数组 .length 取模,取模性能不⾼,位运算的性能⽐较⾼。2、高频并发问题
synchronized实现原理、CAS⽆锁化的原理、AQS是什么、Lock锁、ConcurrentHashMap的分段加锁的原理、线程池的原理、java内存模型、volatile说⼀下吗、对java并发包有什么了解?⼀连串的问题 。
3、synchronized关键字的底层原理是什么?
其实synchronized底层的原理,是跟jvm指令和monitor有关系的
你如果⽤到了 synchronized 关键字,在底层编译后的 jvm 指令中,会有 monitorenter 和 monitorexit 两个指令monitorenter // 代码对应的指令 monitorexit那么 monitorenter 指令执⾏的时候会⼲什么呢? 每个对象都有⼀个关联的monitor,⽐如⼀个对象实例就有⼀个monitor,⼀个类的Class对象也有⼀个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁。 原理: monitor⾥⾯有⼀个计数器,从0开始的。如果⼀个线程要获取monitor的锁,就看看他的计数器是不 是0,如果是0的话,那么说明没⼈获取锁,他就可以获取锁了,然后对计数器加1。
这个monitor的锁是⽀持重⼊加锁的,什么意思呢,好⽐下⾯的代码⽚段
// 线程1
synchronized(myObject) { -> 类的class对象来⾛的
// ⼀⼤堆的代码
synchronized(myObject) {
// ⼀⼤堆的代码
}
}
加锁,⼀般来说都是必须对⼀个对象进⾏加锁。
如果⼀个线程第⼀次synchronized 那⾥,获取到了 myObject 对象的 monitor 的锁,计数器加 1 ,然后第⼆次 synchronized 那⾥,会再次获取myObject 对象的 monitor 的锁,这个就是重⼊加锁了,然后计数器会再次加 1 ,变成2。 这个时候,其他的线程在第⼀次synchronized那⾥,会发现说 myObject 对象的 monitor 锁的计数器是⼤于 0 的,意味着被别⼈加锁了,然后此时线程就会进⼊block 阻塞状态,什么都⼲不了,就是等着获取锁 。 接着如果出了synchronized修饰的代码⽚段的范围,就会有⼀个 monitorexit 的指令,在底层。此时获取锁的线程就会对那个对象的monitor的计数器减 1 ,如果有多次重⼊加锁就会对应多次减 1 ,直到最后,计数器是 0 。 然后后⾯block住阻塞的线程,会再次尝试获取锁,但是只有⼀个线程可以获取到锁 。4、你对CAS的理解以及其底层实现原理可以吗?
多个线程他们可能要访问同⼀个数据 HashMap map = new HashMap();
此时有多个线程要同时读写类似上⾯的这种内存⾥的数据,此时必然出现多线程的并发安全问题。
4.1、synchronized
synchronized(map) {
// 对
map
⾥的数据进⾏复杂的读写处理
}
此时,synchronized
意思就是针对当前执⾏这个⽅法的
myObject
对象进⾏加锁,只有⼀个线程可以成功
的对myObject加锁,可以对他关联的monitor的计数器去加1,加锁,⼀旦多个线程并发的去进⾏synchronized加锁,串⾏化,效率并不是太⾼
,很多线程,都需要排队去执⾏。
}
4.2、CAS
CAS在底层的硬件级别给你保证⼀定是原⼦的,同⼀时间只有⼀个线程可以执⾏ CAS ,先⽐较再设置,其他的线程的 CAS 同时间去执⾏此时会失败。线程1和线程分别读取0到本地内存。线程1货渠到0后+1,然后比较本地值0和内存值0相等,则更新内存值为1。线程2比较本地值0和内存值1不等则更新失败。则尝试设置为2成功。
5、ConcurrentHashMap实现线程安全的底层原理到底是什么?
多个线程要访问同⼀个数据,synchronized加锁,CAS去进⾏安全的累加,去实现多线程场景下的安全的更新⼀个数据的效果,⽐较多的⼀个场景下,可能就是多个线程同时读写⼀个HashMap。使用synchronized,也没这个必要 。
实现1
HashMap的⼀个底层的原理,本⾝是⼀个⼤的⼀个数组,[有很多的元素]
Map map = new Map();
多个线程过来,线程1
要
put
的位置是数组
[5]
,线程
2
要
put
的位置是数组
[21]
map.put(xxxxx,xxx);
明显不好,数组⾥有很多的元素,插入不同位置时可以并发插入的。所以除⾮是对同⼀个元素执⾏put
操作,此时呢需要多线程是需要进⾏同步的。
实现2
JDK并发包⾥推出了⼀个ConcurrentHashMap
,他默认实现了线程安全性。
在JDK 1.7及之前的版本⾥,
分段 即将原数组拆分为多个数组,每个数组分别加锁:
[数组
1] , [
数组
2]
,
[
数组
3] ->
每个数组都对应⼀个锁分段加锁,多个线程过来,
线程1要put的位置是数组1[5],线程2要put的位置是数组2[21]。此时对于不同线程在不同数组中插入数据没有冲突。
JDK 1.8以及之后,做了⼀些优化和改进,锁粒度的细化 。[⼀个⼤的数组
]
,数组⾥每个元素进⾏
put
操作,
都是有⼀个不同的锁,刚开始进⾏put的时候,如果两个线程都是在数组[5]这个位置进⾏put,这个时候,对数组[5]这个位置进⾏put的时候,采取的是CAS的策略
。
同⼀个时间,只有⼀个线程能成功执⾏这个CAS
,就是说他刚开始先获取⼀下数组
[5]
这个位置的值,
null
,然后执⾏
CAS
,线程
1
⽐较⼀下put
进去我的这条数据,同时间其他的线程执⾏
CAS
,都会失败。
分段加锁,通过对数组每个元素执⾏CAS的策略
,如果是很多线程对数组⾥不同的元素执⾏
put
,⼤家是没有关系的,如果其他⼈失败了,其他⼈此时会发现说,数组[5]
这位置,已经给刚才⼜⼈放进去值了。就
需要在这个位置基于链表+红⿊树来进⾏处理,synchronized(数组[5]),加锁,基于链表或者是红⿊树在这个位置
插进去⾃⼰的数据。
如果你是对数组⾥同⼀个位置的元素进⾏操作,才会加锁串⾏化处理;如果是对数组不同位置的元素操作,此时⼤家可以并发执⾏的。
总结:
首先对整个数组加锁 ------》 对数组分段加锁 ------》 对每个元素加锁。
6、你对JDK中的AQS理解吗?AQS的实现原理是什么?
多线程同时访问⼀个共享数据,可以使用sychronized,CAS,ConcurrentHashMap(并发安全的数据结构可以来⽤),Lock等方式处理。
synchronized就有点不⼀样了,底层基于
AQS
,
Abstract Queue Synchronizer
,抽象队列同步器 。以及Semaphore、其他⼀些的并发包下的都是基于AQS。
总结:
首先对整个数组加锁 ------》 对数组分段加锁 ------》 对每个元素加锁。
6、你对JDK中的AQS理解吗?AQS的实现原理是什么?
ReentrantLock lock = new ReentrantLock(true); => ⾮公平锁 // 多个线程过来,都尝试 lock.lock(); lock.unlock();
原理分析:AQS内部的重要变量state=0。此时有多个线程都执行lock.lock()进行加锁。
- 首先线程1和线程2通过CAS去更新state的值为1, 同一时间线程1执行成功(线程1获取state0值,比较0与0相等则更新AQS中的state为1)。此时线程1加锁成功线程2加锁失败。
- 然后AQS内部有一个加锁线程记录加锁成功的线程(加锁线程=线程1)。
- 然后AQS内部有一个等待队列记录加锁失败的线程,故线程2进入等待队列。
- 此时线程1执行成功后释放锁,将state=0,加锁线程=null,唤醒等待队列队头的线程即线程2,线程2开始CAS更新state=1和加锁线程=线程2执行。。。
公平锁和非公平锁
非公平锁:当上图线程2被线程1唤醒后,此时线程3直接先进行CAS将state=1和加锁线程=线程3。然后线程2进行CAS失败继续进入等待队列。故此时线程2时不公平的。
公平锁:当上图线程2被线程1唤醒后,此时线程3首先会判断队列:发现队列有线程2,线程3直接进入等待队列排队。总结
state变量 -> CAS ->
失败后进⼊队列等待
->
释放锁后唤醒 公平与非公平
7、线程池
7.1、说说线程池的底层⼯作原理可以吗?
系统是不可能说让他⽆限制的创建很多很多的线程的,会构建⼀个线程池,有⼀定数量的线程,让他们执⾏各种各样的任务
,线程执⾏完任务之后,不要销毁掉⾃⼰,继续去等待执⾏下⼀个任务。
ExecutorService threadPool = Executors.newFixedThreadPool(3) -> 3: corePoolSize
threadPool.submit(new Callable() {
public void run() {}
})
;
- 提交任务,先看⼀下线程池⾥的线程数量是否⼩于corePoolSize,也就是3,如果⼩于,直接创建⼀个线程出来执⾏你的任务。
- 如果执⾏完你的任务之后,这个线程是不会死掉,他会尝试从⼀个⽆界linkedBlockingQueue⾥获取新的任务,如果没有新的任务,此时就会阻塞住,等待新的任务到来。
- 你持续提交任务,上述流程反复执⾏,只要线程池的线程数量⼩于corePoolSize,都会直接创建新线程来执⾏这个任务,执⾏完了就尝试从⽆界队列⾥获取任务,直到线程池⾥有corePoolSize个线程。
- 接着再次提交任务,会发现线程数量已经跟corePoolSize⼀样⼤了,此时就直接把任务放⼊队列中就可以了,线程会争抢获取任务执⾏的,如果所有的线程此时都在执⾏任务,那么⽆界队列⾥的任务就可能会越来越多。
- newFixedThreadPool的队列是linkedBlockingQueue,一个⽆界阻塞队列。
7.2、线程池的核⼼配置参数都是⼲什么的?平时我们应该怎么⽤?
7.1、说说线程池的底层⼯作原理可以吗?
系统是不可能说让他⽆限制的创建很多很多的线程的,会构建⼀个线程池,有⼀定数量的线程,让他们执⾏各种各样的任务
,线程执⾏完任务之后,不要销毁掉⾃⼰,继续去等待执⾏下⼀个任务。
ExecutorService threadPool = Executors.newFixedThreadPool(3) -> 3: corePoolSize
threadPool.submit(new Callable() {
public void run() {}
})
;
- 提交任务,先看⼀下线程池⾥的线程数量是否⼩于corePoolSize,也就是3,如果⼩于,直接创建⼀个线程出来执⾏你的任务。
- 如果执⾏完你的任务之后,这个线程是不会死掉,他会尝试从⼀个⽆界linkedBlockingQueue⾥获取新的任务,如果没有新的任务,此时就会阻塞住,等待新的任务到来。
- 你持续提交任务,上述流程反复执⾏,只要线程池的线程数量⼩于corePoolSize,都会直接创建新线程来执⾏这个任务,执⾏完了就尝试从⽆界队列⾥获取任务,直到线程池⾥有corePoolSize个线程。
- 接着再次提交任务,会发现线程数量已经跟corePoolSize⼀样⼤了,此时就直接把任务放⼊队列中就可以了,线程会争抢获取任务执⾏的,如果所有的线程此时都在执⾏任务,那么⽆界队列⾥的任务就可能会越来越多。
- newFixedThreadPool的队列是linkedBlockingQueue,一个⽆界阻塞队列。
7.2、线程池的核⼼配置参数都是⼲什么的?平时我们应该怎么⽤?
代表线程池的类是ThreadPoolExecutor。
创建⼀个线程池参数corePoolSize , maximumPoolSize , keepAliveTime queue ,如果你不⽤ fixed 之类的线程池,⾃⼰完全可以通过这个构造函数就创建⾃⼰的线程池。corePoolSize : 3 maximumPoolSize : Integer.MAX_VALUE keepAliveTime : 60s new ArrayBlockingQueue如果说你把queue 做成有界队列,⽐如说 new ArrayBlockingQueue(200)
(1)AbortPolicy (2)DiscardPolicy (3)DiscardOldestPolicy (4)CallerRunsPolicy (5) ⾃定义如果后续慢慢的队列⾥没任务了,线程空闲了,超过corePoolSize 的线程会⾃动释放掉,在 keepAliveTime 之后就会释放。
根据上述原理去定制⾃⼰的线程池,考虑到corePoolSize的数量,队列类型,最⼤线程数量,拒绝策略,线程释放时间
⼀般⽐较常⽤的是:fixed线程。7.3、如果在线程池中使⽤⽆界阻塞队列会发⽣什么问题?
⾯试题:
在远程服务异常的情况下,使⽤⽆界阻塞队列,是否会导致内存异常飙升?
当线程调用远程服务调⽤超时,导致任务处理很慢,而任务进来会很快导致
。
队列变得越来越⼤,此时会导致内存飙升起来,⽽且还可能会导致你会
OOM
,内存溢出。
7.4、如果线程池的队列满了之后,会发⽣什么事情吗?
有界队列,可以避免内存溢出。
corePoolSize: 10
maximumPoolSize : 200
ArrayBlockingQueue(200)
⾃定义⼀个reject
策略,如果线程池⽆法执⾏更多的任务了,此时
建议你可以把这个任务信息持久化写⼊磁盘⾥去,后台专门启动⼀个线程,后续等待你的线程池的⼯作负载降低了,他可以慢慢的从磁盘⾥读取之前持久化的任务,重新提交到线程池⾥去执⾏
。
你可以
⽆限制的不停的创建额外的线程出来,⼀台机器上,有⼏千个线程,甚⾄是⼏万
个线程,
每个线程都有⾃⼰的栈内存,占⽤⼀定的内存资源,会导致内存资源耗尽,系统也会崩溃掉
即使内存没有崩溃,会导致你的机器的cpu load,负载,特别的
⾼。
7.5、如果线上机器突然宕机,线程池的阻塞队列中的请求怎么办?
必然会导致线程池⾥的积压的任务实际上来说都是会丢失的。
如果说你要提交⼀个任务到线程池⾥去,在提交之前,⿇烦你先在数据库⾥插⼊这个任务的信息,更新他的状态:未提交、已提交、已完成。提交成功之后,更新他的状态是已提交状态
。
系统重启,后台线程去扫描数据库⾥的未提交和已提交状态的任务,可以把任务的信息读取出来,重新提交到线程池⾥去,继续进⾏执⾏
。
总结
无界队列不断产生任务:导致内存飙升,内存溢出,OOM
有界队列不断创建线程:线程栈导致内存资源耗尽,cpu负载高。
8、谈谈你对Java内存模型的理解可以吗?
corePoolSize: 10 maximumPoolSize : 200 ArrayBlockingQueue(200)⾃定义⼀个reject 策略,如果线程池⽆法执⾏更多的任务了,此时 建议你可以把这个任务信息持久化写⼊磁盘⾥去,后台专门启动⼀个线程,后续等待你的线程池的⼯作负载降低了,他可以慢慢的从磁盘⾥读取之前持久化的任务,重新提交到线程池⾥去执⾏ 。 你可以 ⽆限制的不停的创建额外的线程出来,⼀台机器上,有⼏千个线程,甚⾄是⼏万 个线程, 每个线程都有⾃⼰的栈内存,占⽤⼀定的内存资源,会导致内存资源耗尽,系统也会崩溃掉 即使内存没有崩溃,会导致你的机器的cpu load,负载,特别的 ⾼。
7.5、如果线上机器突然宕机,线程池的阻塞队列中的请求怎么办?
必然会导致线程池⾥的积压的任务实际上来说都是会丢失的。
如果说你要提交⼀个任务到线程池⾥去,在提交之前,⿇烦你先在数据库⾥插⼊这个任务的信息,更新他的状态:未提交、已提交、已完成。提交成功之后,更新他的状态是已提交状态
。
系统重启,后台线程去扫描数据库⾥的未提交和已提交状态的任务,可以把任务的信息读取出来,重新提交到线程池⾥去,继续进⾏执⾏
。
总结
无界队列不断产生任务:导致内存飙升,内存溢出,OOM
有界队列不断创建线程:线程栈导致内存资源耗尽,cpu负载高。
8、谈谈你对Java内存模型的理解可以吗?
read、load、use / assign、store、write
9、你知道Java内存模型中的原⼦性、有序性、可⻅性是什么?
连环炮:Java内存模型 -> 原⼦性、可⻅性、有序性 -> volatile -> happens-before / 内存屏障
也就是并发编程过程中,可能会产⽣的三类问题9.1、可⻅性 之前⼀直给⼤家代码演示,画图演示,其实说的就是并发编程中可⻅性问题 没有可⻅性: 线程1 将自己内存的值read、load、use、assing、store、write后变成1,而线程2在主内存值变化后没有感知到,仍然使用data=0故一直while休眠。 可见性: 主内存更新后的值被其他线程立马看到。
9.2、原⼦性
有原⼦性,没有原⼦性
原⼦性:
data++
,必须是独⽴执⾏的,没有⼈影响我的,⼀定是我⾃⼰执⾏成功之后,别⼈才能来进⾏下⼀次
data++
的执⾏。
9.3、有序性
对于代码,同时还有⼀个问题是指令重排序,编译器和指令器,有的时候为了提⾼代码执⾏效率,会将指令重排序,就是说⽐如下⾯的代码。
具备有序性:不会发⽣指令重排导致我们的代码异常; 不具备有序性:可能会发⽣⼀些指令重排,导致代码可能会出现⼀些问题。 重排序之后,让flag = true 先执⾏了,会导致线程 2 直接跳过 while 等待,执⾏某段代码,结果 prepare() ⽅法还没执⾏,资源还没准备好呢,此时就会导致代码逻辑出现异常。10、能从Java底层⾓度聊聊volatile关键字的原理吗?
内存模型 -> 原⼦性、可见性、有序性 -> volatile
讲清楚volatile 关键字,直接问你 volatile 关键字的理解,对前⾯的⼀些问题,这个时候你就应该⾃⼰去主动从内存模型开始讲起,原⼦性、可见性、有序性的理解,volatile 关键字的原理。 volatile关键字是⽤来解决可见性和有序性,在有些罕见的条件之下,可以有限的保证原⼦性,他主要不是⽤来保证原⼦性的。volatile修饰的变化,会保证在其他线程工作内存中的该变量值失效。故线程2会强制从主内存读data数据:read/load/use。
在很多的开源中间件系统的源码⾥,⼤量的使⽤了volatile,每⼀个开源中间件系统,或者是⼤数据系统,都多线程并发,volatile
当脚本修改running为false后,添加了volatile能保证主线程
11、你知道指令重排以及happens-before原则是什么吗?
volatile关键字和有序性的关系,volatlie是如何保证有序性的,如何避免发⽣指令重排的。
下面代码如果没有有序性,可能prepare()和flag=true顺序更改,会导致线程2在没有等线程1准备好资源的情况下进行操作。
java中有⼀个 happens-before 原则: 编译器、指令器可能对代码重排序,乱排,要守⼀定的规则,happens-before 原则,只要符合 happens-before 的原则,那么就不能胡乱重排,如果不符合这些规则的话,那就可以⾃⼰排序。- 程序次序规则:⼀个线程内,按照代码顺序,书写在前⾯的操作先⾏发⽣于书写在后⾯操作
- 锁定规则:⼀个unLock操作先⾏发⽣于后⾯对同⼀个锁的lock操作,⽐如说在代码⾥有先对⼀个lock.lock(),lock.unlock(),lock.lock()。
- volatile变量规则:对⼀个volatile变量的写操作先⾏发⽣于后⾯对这个volatile变量的读操作,volatile变量写,再是读,必须保证是 先写,再读。
- 传递规则:如果操作A先⾏发⽣于操作B,⽽操作B⼜先⾏发⽣于操作C,则可以得出操作A先⾏发⽣于操作C
- 线程启动规则:Thread对象的start()⽅法先⾏发⽣于此线程的每个⼀个动作,thread.start()在thread.interrupt()前。
- 线程中断规则:对线程interrupt()⽅法的调⽤先⾏发⽣于被中断线程的代码检测到中断事件的发⽣
- 线程终结规则:线程中所有的操作都先⾏发⽣于线程的终⽌检测,我们可以通过Thread.join()⽅法结束、Thread.isAlive()的返回值,⼿段检测到线程已经终⽌执⾏
- 对象终结规则:⼀个对象的初始化完成先⾏发⽣于他的finalize()⽅法的开始
⽐如这个例⼦,如果⽤volatile来修饰flag变量,⼀定可以让prepare()指令在flag = true之前先执⾏,这就禁⽌了指令重排。因为volatile要求的是,volatile前⾯的代码⼀定不能指令重排到volatile变量操作后⾯,volatile后⾯的代码也不能指令重排到volatile前⾯。
指令重排 -> happens-before -> volatile 起到避免指令重排12、volatile底层是如何基于内存屏障保证可⻅性和有序性的?
连环炮:内存模型 -> 原⼦性、可⻅性、有序性 - > volatile+可⻅性 -> volatile+有序性(指令重排 + happens-before) -> voaltile+原⼦性 -> volatile底层的原理(内存屏障级别的原理)
1、volatile + 原⼦性: 不能够保证原⼦性,虽然说有些极端特殊的情况下有保证原⼦性的效果,杠精拿着⼀些极端场景下例⼦说volatile也可以原⼦性, oracle , 64 位的 long 的数字进⾏操作。 保证原⼦性: synchronized,lock,加锁 。 2、volatile底层原理,如何实现保证可⻅性的呢?如何实现保证有序性的呢? (1)lock指令:volatile保证可⻅性 对volatile 修饰的变量,执⾏ 写操作的话,JVM会发送⼀条lock前缀指令给CPU , CPU 在计算完之后会⽴即将这个值写回主内存,同时因为有 MESI缓存⼀致性协议 ,所以 各个CPU都会对总线进⾏嗅探,⾃⼰本地缓存中的数据是否被别⼈修改 。如果 发现别⼈修改了某个缓存的数据,那么CPU就会将⾃⼰本地缓存的数据过期掉,然后这个CPU上执⾏的线程在读取那个变量的时候,就会从主内存重新加载最新的数据 了。 lock前缀指令 + MESI缓存⼀致性协议 。 (2)内存屏障:volatile禁⽌指令重排序 volatille是如何保证有序性的? 加了volatile 的变量,可以保证前后的⼀些代码不会被指令重排,这个是如何做到的呢?指令重排是怎么回事,volatile 就不会指令重排。Load1 : int localVar = this.variable Load2 : int localVar = this.variable2 LoadLoad 屏障: Load1 ; LoadLoad ; Load2 ,确保 Load1 数据的装载先于 Load2 后所有装载指令,他的意思, Load1 对应的代码和 Load2 对应的代码,是不能指令重排的 Store1 this.variable = 1 StoreStore 屏障 Store2 : this.variable2 = 2 StoreStore 屏障: Store1 ; StoreStore ; Store2 ,确保 Store1 的数据⼀定刷回主存,对其他 cpu 可⻅,先于 Store2 以及后续指令 LoadStore 屏障: Load1 ; LoadStore ; Store2 ,确保 Load1 指令的数据装载,先于 Store2 以及后续指令 StoreLoad 屏障: Store1 ; StoreLoad ; Load2 ,确保 Store1 指令的数据⼀定刷回主存,对其他 cpu 可⻅,先于 Load2 以及后续指令的数 据装载 volatile 的作⽤是什么呢? volatile variable = 1 this.variable = 2 => store 操作 int localVariable = this.variable => load 操作对于volatile 修改变量的读写操作,都会加⼊内存屏障 。每个volatile 写操作前⾯,加 StoreStore 屏障,禁⽌上⾯的普通写和他重排;每个 volatile 写操作后⾯,加 StoreLoad 屏障,禁⽌跟下⾯的。 volatile读/ 写重排 :每个volatile 读操作后⾯,加 LoadLoad 屏障,禁⽌下⾯的普通读和 voaltile 读重排;每个 volatile 读操作后⾯,加 LoadStore 屏障,禁⽌下⾯的普通写和volatile 读重排。



