什么是死锁?
两个线程或两个以上线程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是这些线程都陷入了无限的等待中。 例如,如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,并且它们永远也不会知道发生了这样的事情。为了得到彼此的对象(A和B),它们将永远阻塞下去。这种情况就是一个死锁。
public class DeadLock implements Runnable{
public int flag = 1;
private static Object object1 = new Object();
private static Object object2 = new Object();
@Override
public void run() {
System.out.println("The Flag is :"+flag);
if(flag == 1){
synchronized (object1){
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (object2){
System.out.println("The object2 is locked!");
}
}
} else if(flag == 2){
synchronized (object2){
try {
Thread.sleep(2000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (object1){
System.out.println("The object1 is locked!");
}
}
}
}
public static void main(String[] args) {
DeadLock deadLock1 = new DeadLock();
DeadLock deadLock2 = new DeadLock();
deadLock1.flag = 1;
deadLock2.flag = 0;
//deadLock1,deadLock2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
//deadLock2的run()可能在deadLock1的run()之前运行
new Thread(deadLock1).start();
new Thread(deadLock2).start();
}
}
死锁的四个条件
1、互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
2、请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3、不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4、循环等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。
预防死锁
预防死锁的四种方法
破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。
破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。
破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只适用于内存和处理器资源。
破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
设置加锁顺序
如果两个线程(A和B),当A线程已经锁住了Z,而又去尝试锁住X,而X已经被线程B锁住,线程A和线程B分别持有对应的锁,而又去争夺其他一个锁(尝试锁住另一个线程已经锁住的锁)的时候,就会发生死锁,如下图:
两个线程试图以不同的顺序来获得相同的锁,如果按照相同的顺序来请求锁,那么就不会出现循环的加锁依赖性,因此也就不会产生死锁,每个需要锁Z和锁X的线程都以相同的顺序来获取Z和X,那么就不会发生死锁了,如下图所示:
这样死锁就永远不会发生。 针对两个特定的锁,可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,我们通过设置锁的顺序,来防止死锁的发生,在这里我们使用System.identityHashCode方法来定义锁的顺序,这个方法将返回由Obejct.hashCode 返回的值,这样就可以消除死锁发生的可能性。
import javax.naming.InsufficientResourcesException;
class Money{
private int count;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public void addCount(int count){
this.count += count;
}
public void subCount(int count){
this.count -= count;
}
public boolean compareTo(int tranCount){
return tranCount>count?false:true;
}
}
public class DeadLockTest {
private static final Object lock = new Object();
// 这个方法容易发生动态的锁顺序死锁,这个是错误的方法
public void tranForMoney(Money fromMoney,Money toMoney,int money) throws InsufficientResourcesException {
synchronized (fromMoney){
synchronized (toMoney){
if(fromMoney.compareTo(money)){
throw new InsufficientResourcesException();
}else {
fromMoney.subCount(money);
toMoney.addCount(money);
}
}
}
}
// 通过锁顺序来避免死锁——正确方法
public void tranFerMoney2(Money fromMoney,Money toMoney,int money) throws InsufficientResourcesException {
class Helper{
public void tranfer() throws InsufficientResourcesException {
if(fromMoney.compareTo(money)){
throw new InsufficientResourcesException();
}else {
fromMoney.subCount(money);
toMoney.addCount(money);
}
}
}
int fromHashCode = System.identityHashCode(fromMoney);
int toHashCode = System.identityHashCode(toMoney);
if(fromHashCode < toHashCode){
synchronized (fromMoney){
synchronized (toMoney){
new Helper().tranfer();
}
}
}else if(fromHashCode > toHashCode){
synchronized (toMoney){
synchronized (fromMoney){
new Helper().tranfer();
}
}
}else {
// 设置时赛锁
synchronized (lock){
synchronized (fromMoney){
synchronized (toMoney){
new Helper().tranfer();
}
}
}
}
}
}
在极少数情况下,两个对象可能拥有两个相同的散列值,此时必须通过某种任意的方法来决定锁的顺序,否则可能又会重新引入死锁。为了避免这种情况,可以使用 “加时(Tie-Breaking))”锁 ,这获得这两个Account锁之前,从而消除了死锁发生的可能性
避免死锁之银行家算法
银行家算法概述
银行家算法是一种最有代表性的避免死锁的算法。在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,这就需要引入安全序列来进行判断资源分配是否合理。安全序列是指一个进程序列比如从P1到Pn是安全的,即对于每一个进程Pi(1≤i≤n),它以后尚需要的资源量不超过系统当前剩余资源量与所有进程Pj (j < i )当前占有资源量之和。如果存在有一个由系统中所有进程构成的安全序列P1…Pn,则系统处于安全状态系统处于安全状态则说明系统的资源分配合理,进程运行的过程中不存在死锁,若分配不会导致系统进入不安全状态,则分配,否则等待。
实现银行家算法过程
首先执行资源预分配算法,设Request[i] 是进程Pi的请求向量,如果Request[i,j]=K,表示进程需要K个Rj类型的资源。当Pi发出资源请求后,系统按下述步骤进行检查:
(1)判断进程请求的资源是否超过进程以后还需要的资源矩阵, 即 Request[i,j]<= Need[i,j]若条件不成立,则认为系统无法分配,因为它需要的最大资源数已超过它所宣布的最大值。
(2)判断进程请求资源是否超过系统现有的资源, 即Request[i,j]<= Available[j],如果条件不成立,则系统现有的资源无法满足当前进程请求的资源数,进程需要被设置为等待状态。
(3)系统试探着把资源分配给进程Pi, 并修改下面数据结构中的数值:
Available[j]= Available[j]- Request[i,j];
Allocation[i,j]= Allocation[i,j]+ Request[i,j];
Need[i,j]= Need[i,j]- Request[i,j];
(4)系统执行安全性算法, 检查此次资源分配后,系统是否处于安全状态。若安全,才正式将资源分配给进程Pi,以完成本次分配;否则,将本次的试探分配作废,恢复原来的资源分配状态,让进程Pi等待。 进程资源预分配完成后,系统需要执行的安全性算法,安全性算法执行的具体步骤如下:
(1)设置两个向量:①工作向量Work:它表示系统可提供给进程继续运行所需要的各类资源数目,它含有m个元素,在执行安全算发开始时,Work=Available;②Finish:它表示系统是否有足够的资源分配给进程,使之运行完成。开始时先做Finish[i]=false;当有足够资源分配给进程时,再令Finish[i]=true。
(2)从进程集合中找到一个能满足下述条件的进程:Finish[i]=false; Need[i,j] <= Work[j];若找到,执行步骤(3),否则,执行步骤(4)。
(3)当进程Pi获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,故应执行:
Work[j]= Work[i]+ Allocation[i,j];
Finish[i]=true;
(4)如果所有进程的Finish[i]=true都满足,则表示系统处于安全状态;否则,系统处于不安全状态。
BankerTest.class
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BankerTest {
private static List<Integer> Available = new ArrayList<>();
private static List<List<Integer>> Max = new ArrayList<>();
private static List<List<Integer>> Allocation = new ArrayList<>();
private static List<List<Integer>> Need = new ArrayList<>();
// 银行家算法
public static void MockBanker(RequestData data) {
// 获取进程ID
int id = data.getId();
boolean Error = false ,Wait = false;
int len = data.getRequestList().size();
// 获取资源请求列表
List<Integer> request = data.getRequestList();
for (int i = 0; i < len; i++) {
if(request.get(i) > Need.get(id).get(i)){ // 判断 Requesti[j] ≤ Need[i,j]
Error = true;
}else if(request.get(i) > Available.get(i)){ // 判断 Requesti[j] ≤ Available[j]
Wait = true;
}
}
if(Error){
System.out.println("进程P"+id+"申请资源"+request+"无法分配");
return;
}else if(Wait){
System.out.println("进程P"+id+"申请资源"+request+"无法满足");
return;
}
// 尝试资源预分配
// 当前系统中已经分配的资源
List<Integer> currentAllocation = Allocation.get(id);
// 当前进程仍需要的资源
List<Integer> currentNeed = Need.get(id);
int length = currentAllocation.size();
for (int i = 0; i < length; i++) {
Available.set(i,Available.get(i)-request.get(i));
currentAllocation.set(i,currentAllocation.get(i)+request.get(i));
currentNeed.set(i,currentNeed.get(i)-request.get(i));
}
Allocation.set(id,currentAllocation);
Need.set(id,currentNeed);
// 安全性检查
if(!SourceSecurityCheck()){
// 如果安全性检查不通过,则执行资源回滚操作
for (int i = 0; i < length; i++) {
Available.set(i,Available.get(i)+request.get(i));
currentAllocation.set(i,currentAllocation.get(i)-request.get(i));
currentNeed.set(i,currentNeed.get(i)+request.get(i));
}
Allocation.set(id,currentAllocation);
Need.set(id,currentNeed);
System.out.println("进程P"+id+"请求的系统资源:"+request+"后,无法保证进程的安全性!");
}else { // 安全性检查通过,成功分配资源。
System.out.println("进程P"+id+"请求的系统资源:"+request+"分配成功!");
}
}
// 安全性检查
public static boolean SourceSecurityCheck(){
// 初始化Work和Finish矩阵
List<Integer> Work = new ArrayList<>();
List<Boolean> Finish = new ArrayList<>();
Work.addAll(Available);
int max = Max.size();
for (int i = 0; i < max; i++) {
Finish.add(i,false);
}
int FinishCount; //判断资源分配完成多少个
for (FinishCount = 0;FinishCount < max;FinishCount++){
for (int i = 0; i < max; i++) {
if(!Finish.get(i)){// 判断没完成系统安全性检查的进程
List<Integer> currentNeed = Need.get(i);
int j;
for (j = 0; j < Work.size(); j++) {
if(currentNeed.get(j) > Work.get(j)){
break;
}
}
if(j == Work.size()){
List<Integer> currentAllocation = Allocation.get(i);
for (j = 0; j < Work.size(); j++) {
Work.set(j,Work.get(j)+currentAllocation.get(j));
}
Finish.set(i,true);
System.out.println("进程P"+i+"执行完毕");
}
}
}
}
for(int id = 0; id < max; id++){
if(!Finish.get(id)){
return false;
}
}
return true;
}
public static void printSourceAllocation(){
System.out.println("Max\t\t\tAllocation\tNeed");
for (int i = 0; i < Max.size(); i++) {
System.out.println(Max.get(i)+"\t"+Allocation.get(i)+"\t"+Need.get(i));
}
System.out.println("当前可用资源为:"+Available);
}
public static void main(String[] args){
Integer[][] maxArray = {{7,5,3}, {3,2,2}, {9,0,2}, {2,2,2}, {4,3,3}};
for (Integer[] tmpArray : maxArray) {
Max.add(Arrays.asList(tmpArray));
}
Integer[][] allocationArray = {{0,1,0}, {2,0,0}, {3,0,2}, {2, 1, 1}, {0,0,2}};
for (Integer[] tmpArray : allocationArray) {
Allocation.add(Arrays.asList(tmpArray));
}
int len = maxArray.length;
int temp = maxArray[0].length;
Integer[][] needArray = new Integer[len][temp];
for (int i = 0; i < len; i++) {
for (int j = 0; j < temp; j++) {
needArray[i][j] = maxArray[i][j]-allocationArray[i][j];
}
}
for (Integer[] tmpArray : needArray) {
Need.add(Arrays.asList(tmpArray));
}
Integer[] availableArray = {3,3,2};
Available = Arrays.asList(availableArray);
System.out.println("第一问:");
printSourceAllocation();
System.out.println("安全性检查结果为:"+SourceSecurityCheck());
System.out.println();
System.out.println("第二问:");
RequestData request = new RequestData();
request.setId(1);
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(0);
list.add(2);
request.setRequestList(list);
printSourceAllocation();
MockBanker(request);
System.out.println();
System.out.println("第三问:");
RequestData request1 = new RequestData();
request1.setId(3);
List<Integer> list1 = new ArrayList<>();
list1.add(3);
list1.add(3);
list1.add(0);
request1.setRequestList(list1);
printSourceAllocation();
MockBanker(request1);
System.out.println();
System.out.println("第四问: ");
RequestData request2 = new RequestData();
request2.setId(0);
List<Integer> list2 = new ArrayList<>();
list2.add(0);
list2.add(2);
list2.add(0);
request2.setRequestList(list2);
printSourceAllocation();
MockBanker(request2);
System.out.println();
}
}
RequestData.class
import java.util.List;
public class RequestData{
private int id;
private List<Integer> requestList;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public List<Integer> getRequestList() {
return requestList;
}
public void setRequestList(List<Integer> requestList) {
this.requestList = requestList;
}
}
CPU术语定义
内存屏障: 英文翻译为memory barriers,它是一组处理器指令,用于实现对内存操作的顺序限制。
缓冲行: 英文翻译为cache line,缓存中可以分配的最小的存储单位,处理器填写缓存线时会加载整个缓存线,需要使用多个主内存读周期
原子操作: 英文翻译为 atomic operation,不可中断的一个或一系列的操作。
缓存行填充: 英文翻译为 cache fill line ,当处理器识别到从内存中读取操作数是可以缓存的,处理器读取整个缓存行到适当的缓存(L1,L2,L3的或所有)
缓存命中: 英文翻译为 cache hit ,如果进行高速缓存行填充操作的内存位置仍然是下一次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存中读取。
写命中: 英文翻译为write hit,当处理器将操作数写回到一个内存缓仔的区域时,匕目元公巴旦心个缓存的内存地址是否在缓存行中,如果存在一个有效的缓仔仃,则处理器将这个操作数写回到缓存,而不是写回到内存,这个操作被称写命中。
写缺失: 英文翻译为 write miss the cache, 一个有效的缓存行被写入到不存在的内存区域。
volatile的两条实现原则
1、Lock前缀指令会引起处理器缓存写入内存: 它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
2、一个处理器的缓存回写到内存中会导致其他处理器的缓存无效: 处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如,在Pentium和P6 family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。
volatile关键字的定义和实现原理
volatile 可以看作是轻量级的synchronized ,java的volatile关键字在多处理器开发中保证了共享变量的“可见性”,什么是可见性?可见性是当一个线程修改当前的共享变量时,另外一个线程可以看到和读取到这个共享变量变化后的值。而且volatile关键字的使用恰当,可以不引起线程的上下文切换和调度。这可比synchronized的使用和执行成本更低。
volatile的可见性实现原理
volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现:
- 内存屏障,又称内存栅栏,是一个 CPU 指令。用于实现对内存操作的顺序限制。
- 在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止+ 特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。
内存屏障
内存屏障是被插入两个指令之间进行使用,其作用是禁止处理器编译器进行指令重排序从而保证了指令的有序性。
按照可见性保障划分: 内存屏障可分为加载屏障(Load Barrier)和存储屏障(Store Barrier)。加载屏障的作用是刷新处理器缓存,存储屏障的作用冲刷处理器缓存。Java虚拟机会在MonitorExit(释放锁)对应的机器码指令之后插入一个存储屏障,这就保障了写线程在释放锁之前在临界区中对共享变量所做的更新对读线程的执行处理器来说是可同步的。JAVA虚拟机会在MonitorEnter(申请锁)对应的机器码指令之后临界区开始之前的地方插入一个加载屏障,这使得读程序的执行处理器能够将写线程对相应共享变量所作的更新从其他处理器同步到该处理的高速缓存中。
按照有序性保障划分: 可分为获取屏障(Acquire Barrier)和释放屏障(Release Barrier)。获取屏障的作用是在一个读操作之后插入该内存屏障,其作用是禁止该读操作与其后的任何读写操作之间进行重排序,这相当于进行的后续操作之前先要获得相应共享数据的所有权。释放屏障的作用是在写操作之前插入该内存屏障,其作用是禁止该写操纵与其前面的任何读操作进行重排序,这相当于在对应共享数据操作结束后释放所有权。Java虚拟机会在MonitorEnter对应的机器码指令之后临界区开始之前的地方插入一个获取屏障,并在临界区结束之后MonitorExit对应的机器码指令之前的地方插入一个释放屏障。
Lock指令
在 Pentium 和早期的 IA-32 处理器中,lock 前缀指令会使处理器执行当前指令时产生一个 LOCK# 信号,会对总线进行锁定,其它 CPU 对内存的读写请求都会被阻塞,直到锁释放。后来的处理器,加锁操作是由高速缓存锁代替总线锁来处理。因为锁总线的开销比较大,锁总线期间其他 CPU 没法访问内存。这种场景多缓存的数据一致通过缓存一致性协议(MESI)来保证。
缓存一致性协议
缓存是分段(line)的,一个段对应一块存储空间,称之为缓存行,它是 CPU 缓存中可分配的最小存储单元,大小 32 字节、64 字节、128 字节不等,这与 CPU 架构有关,通常来说是 64 字节。LOCK# 因为锁总线效率太低,因此使用了多组缓存。为了使其行为看起来如同一组缓存那样。因而设计了 缓存一致性协议。缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于 " 嗅探(snooping)" 协议。所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线。缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个 CPU 缓存可以读写内存)。CPU 缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器写内存,其它处理器马上知道这块内存在它们的缓存段中已经失效。
CPU高速缓存
CPU 在执行指令时需要从内存获取指令和所需的数据,但是 CPU 的速度要远大于内存速度,所以 CPU 直接从内存中存取数据要等待一定时间周期,造成资源的浪费并且影响性能。
这个时候就需要引入CPU高速缓存器,CPU高速缓存(Cache Memory)是用于减少处理器访问内存时所需要平均时间的部件,位于CPU与内存之间,它的容量远远小于内存。
当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。缓存之所以有效,主要是因为程序运行时对内存的访问呈现局部性(Locality)特征。这种局部性既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality)。有效利用这种局部性,缓存可以达到极高的命中率。
时间局部性:如果某个数据被访问,那么不久的将来他很可能被再次访问。 空间局部性:如果某个数据被访问,那么与他相邻的数据很快也可能被问。
CPU高速缓存构造图:
实例代码:
public class Demo {
private volatile int i;
public void update(){
i = 100;
}
public static void main(String[] args) {
Demo demo = new Demo();
demo.update();
System.out.println(demo.i);
}
}
结果: 使用hsdis和JITWatch可以得到汇编代码:hsdis和JITWatch的资源可以从我的百度网盘中下载:提取码:vbnn
0x000002620a035723: and $0xffffffffffffff87,%rdi
0x000002620a035727: je 0x000002620a0357b8
0x000002620a03572d: test $0x7,%rdi
0x000002620a035734: jne 0x000002620a03577d
0x000002620a035736: test $0x300,%rdi
0x000002620a03573d: jne 0x000002620a03575c
0x000002620a03573f: and $0x37f,%rax
0x000002620a035746: mov %rax,%rdi
0x000002620a035749: or %r15,%rdi
0x000002620a03574c: lock cmpxchg %rdi,(%rdx) // 在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
0x000002620a035751: jne 0x000002620a035bd5
0x000002620a035757: jmpq 0x000002620a0357b8
0x000002620a03575c: mov 0x8(%rdx),%edi
0x000002620a03575f: shl $0x3,%rdi
0x000002620a035763: mov 0xa8(%rdi),%rdi
0x000002620a03576a: or %r15,%rdi
volatile的有序性实现原理
happens-before 的定义
happens-before的基本作用是指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happensbefore关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。
禁止指令重排序
为了性能优化,JMM 在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序。JMM 提供了内存屏障阻止这种重排序。 Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。 JMM 会针对编译器制定 volatile 重排序规则表(No表示禁止重排序)。
为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。 对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM 采取了保守的策略。
- 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
- 在每个 volatile 写操作的后面插入一个StoreLoad 屏障。
- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
- 在每个 volatile读操作的后面插入一个 LoadStore 屏障。
常见volatile关键字问题
1、volatile关键字的作用是什么?
2、volatile能保证原子性吗??
3、之前32位机器上共享的long和double变量的为什么要用volatile?
4、现在64位机器上是否也要设置呢?
5、i++为什么不能保证原子性?
6、volatile是如何实现可见性的?
7、volatile是如何实现有序性的?
8、说下volatile的应用场景?
9、volatile 变量和 atomic 变量有什么不同?
以上是volatile关键字的常见问题,后续会对这些问题进行详细解答。
问题解析
禁止指令重排序
首先来一个很经典的重排序问题,那就是如何实现单例模式,那么在并发的环境下实现单例模式,那么通常可以通过双重加锁的方式(DCL)来实现。
/**
* @author: 随风飘的云
* @describe:
* @date 2022/03/28 0:13
*/
public class Reordering {
public static volatile Reordering instance;
/**
* 私有构造函数,防止外部实例化
*/
private Reordering(){}
public static Reordering getInstance(){
if(instance == null){
synchronized (Reordering.class){
if(instance == null){
instance = new Reordering();
}
}
}
return instance;
}
}
构造一个对象需要三个步骤
1、分配内存空间 2、初始化对象 3、将内存空间的地址赋值给对应的地址引用。
假如这个时候发送了指令重排序,那么构造一个对象的三个步骤可能变成这样子:
1、分配内存空间 2、将内存空间的地址赋值给对应的地址引用。 3、初始化对象
如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。
实现可见性
volatile 关键字是如何保证可见性的?首先来看下面的代码:
instance = new Singleton(); // instance是volatile变量
可以查看使用JIT编译器生成的汇编指令来查看对volatile关键字修饰的变量进行写操作是,CPU会做什么?转变成汇编代码,如下:
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
可以看出有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,Lock前缀的指令在多核处理器下会引发了两件事情:
1、将当前处理器缓存行的数据写回系统内存中 2、这个写会内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高运行速度,处理器不直接与内存通信,而是将系统内存中的数据写入到CPU的内部缓存中再进行其他的操作。如果对volatile关键字修饰了的变量进行写操作,JVM虚拟机就会向处理器发送一条Lock指令,将变量所在的缓存行写入到系统内存中,每个处理器需要检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。这样就实行了缓存一致性协议。
实例代码:
/**
* @author: 随风飘的云
* @describe:
* @date 2022/03/28 0:13
*/
public class Reordering {
private int a = 1;
private int b = 2;
public void change(){
a = 3;
b = a;
}
public void print(){
System.out.println("a: " + a + " b: " + b);
}
public static void main(String[] args) {
while (true){
final Reordering reordering = new Reordering();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
}catch (InterruptedException exception){
exception.printStackTrace();
}
reordering.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
}catch (InterruptedException exception){
exception.printStackTrace();
}
reordering.print();
}
}).start();
}
}
}
结果:
代码分析:
正常来说,上面的代码只会出现 a = 1,b = 2或者是 a = 3, b = 3,因为如果先执行change方法,再执行print方法,输出结果应该为b=3,a=3。相反,如果先执行的print方法,再执行change方法,结果应该是 b=2, a=1。不应该出现其他的结果了,奇怪的是上面会出现 a = 1,b = 3的结果,这是因为第一个线程修改了 a 的值,这个值对于第二个线程来说是不可见的。如果将a和b都改成volatile类型的变量再执行,则再也不会出现b=3;a=1的结果了。
保证原子性
严格意义上来说,volatile关键字仅是实现变量的修改可见性,并不能完全保证原子性,仅仅只是可以保证读写操作的单次操作具有原子性。可以这般理解,volatile关键字实现可见性的本质是告诉jvm虚拟机使用volatile关键字修饰的当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取,而synchronized则是锁定当前变量,只有当前变量可以访问改变量,其他线程则被阻塞,这样子,synchronized可以实现可见性和原子性,这也是为什么上文中说到volatile是轻量级的synchronized了。
问题1:i ++为什么不可以保证原子性
volatile变量的单次读/写操作是可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。
实例代码:
/**
* @author: 随风飘的云
* @describe:
* @date 2022/03/28 1:17
*/
public class VolatileTest {
private volatile int a = 0;
public void change(){
a ++;
}
public static void main(String[] args) throws InterruptedException {
final VolatileTest test = new VolatileTest();
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
}catch (InterruptedException exception){
exception.printStackTrace();
}
test.change();
}
}).start();
}
Thread.sleep(10000);
System.out.println(test.a);
}
}
结果:
代码分析:
上面的结果有点强差人意,正常来说a的值应当是1000,因为使用了volatile关键字修饰了a变量。但是a的值仅仅是967。这就存在了一个问题了,volatile关键字仅仅只是可以保证单次的读写操作具有原子性,但是不保证类似于 i ++这种复合操作具有原子性。i ++ 的操作步骤可以划分为三个:
1、读取 i 的值 2、将 i 的值加1 3、将 i 写回内存中
volatile是无法保证这三个操作是具有原子性的,但是可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。
共享的long和double变量的需要用volatile 首先对于long类型或者是double类型的变量具有高32位或是低32位的操作,那么对于这两种类型的共享变量的读写操作是不能保证具有原子性的,因此需要使用volatile关键字修饰该变量,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。
volatile和synchronized的区别
volatile 的应用场景
使用 volatile 必须具备的条件
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
- 只有在状态真正独立于程序内其他内容时才能使用 volatile。
场景1:状态标志
实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
volatile boolean shutdownRequested;
......
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
场景2:一次性安全发布(one-time safe publication)
缺乏同步会导致无法实现可见性,这使得确定何时写入对象引用而不是原始值变得更加困难。在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另一个线程写入)和该对象状态的旧值同时存在。(这就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造的对象)。
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;
public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}
public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}
场景3: 独立观察(independent observation)
安全使用 volatile 的另一种简单模式是定期 发布 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
public class UserManager {
public volatile String lastUser;
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}
场景4: volatile bean 模式
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。
@ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
场景5:开销较低的读-写锁策略
volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
@ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}
场景6: 双重检查(double-checked)
单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。
class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}