一, volatile
1. 内存不可见性
JMM(java内存模型) 是抽象的概念,描述多线程与内存间的通信,Java线程内存模型与CPU缓存模型类似,是标准化的,用于屏蔽底层的内存访问差异。
Java中,每个线程都有自己独立的工作内存,存储共享变量的副本,改变单个线程的变量副本不会影响到其他线程中的副本。
我们来看这样一份代码
public class voil {
private static boolean flag=false;
public static void refresh(){
System.out.println("refreshing data...");
flag=true;
System.out.println("refreshion complete");
}
public static void loadData(){
while(!flag);
System.out.println("flag had been changed");
}
public static void main(String[]args){
Thread th0 = new Thread(()->{loadData();},"ThreadA");
Thread th1 = new Thread(()->{refresh();},"ThreadB");
th0.start();
try{
Thread.sleep(300);
}catch(InterruptedException e){
e.printStackTrace();
}
th1.start();
}
}
其执行结果如下
refreshing data...
refreshion complete
[]
在th1执行完毕后,th1只在其线程内部更改了flag的值,而并未更改主内存区的flag值,所以线程th0中继续死循环。
解决方法如下,下节我们将开始通过8大原子操作解释volatile。
private static volatile boolean flag=false;
结果:
refreshing data...
refreshion complete
flag had been changed
2. 内存交互的8大原子操作
为了工作内存与主内存的交互,java内存模型定义了8类原子操作。
- lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
- read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
- load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
- use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
- assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
- store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
- write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
- unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
参考:
3. MESI缓存一致性协议
MESI协议的四种状态
- M: 被修改(Modified)
该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。
- E: 独享的(Exclusive)
该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。
- S: 共享的(Shared)
该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。
- I: 无效的(Invalid)
该缓存是无效的(可能有其它CPU修改了该缓存行)。
状态跃迁图
第一节中volatile作用过程
线程0加载S类型的变量后,若要修改该变量,则线程0对工作内存中的变量加锁,同时向总线发送修改消息。同时线程1嗅探到该消息,并将该变量状态置为I(Invalid)。若线程1需要读该变量,则向总线发送消息,线程0读取到该消息,立刻将该值写回主内存并向线程1发送可读消息,并解锁工作内存变量。线程1读取主内存变量后,向线程0发送已读信号,线程0接收信号后将变量状态重新置为S(Share)。
volatile原理
JMM内存交互 volatile修饰的变量的read,load,use操作和assgin,store,write必须是连续的,即修改后必须立即同不回主存,使用时必须从主内存刷新,以此保证volatile变量的可见性。
底层实现 通过lock前缀指令,锁定 变量缓存行 区域并回写主存,又称为“缓存锁定”。
- 缓存一致性协议会阻止两个以上的处理器同时修改主存的数据。
- 单个处理器缓存回写到主存后会导致其他处理器的缓存无效。
参考:
4. 非原子性数据的缓存一致性
我们再来看这样一份代码,理论上,counter的输出值应该是1000*10=10^4
public class ex4 {
private static volatile int counter=0;
private static void compute(){
counter++;
}
public static void main(String[]args){
for(int i=0;i<10;i++){
String name = "Thread"+i;
Thread t0 = new Thread(()->{
for(int k=0;k<1000;k++)
compute();
},name);
t0.start();
}
try{
Thread.sleep(3000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(counter);
}
}
可实际的输出情况为:
9990
PS C:\Users\w1738\Desktop\Classfied\JavaMultiThreadTest>
原因
出现这种情况的根本原因是counter++操作本身不是原子的,它分为三步:将counter读入工作缓存;对其进行++;回写到主内存。这三步的JVM指令为:
mov 0xc(%r10),%r8d ; Load
inc %r8d ; Increment
mov %r8d,0xc(%r10) ; Store
lock addl $0x0,(%rsp) ; StoreLoad Barrier
注意最后一步是内存屏障(Memeory Barrier),该指令可以确保特定操作的执行顺序。对于volatile,Java内存模型将在写操作 后 插入写内存屏障,若是读操作,将在该操作 前 插入读内存屏障。这意味着voaltile可以保证一旦完成写入,任何访问该变量的线程将得到最新的值,而在写入前,保证所有该在之前的操作完成。
在线程1读入counter=0后被阻塞,随后线程2开始执行,并读入counter=0,自加,并更新主存中的counter=1。
线程1再次开始执行,此时因为之前还有自加和写入主存的操作没有完成,而内存屏障保证该操作必须在接下来优先执行。所以在线程1中自加counter=1,并写回主存。
总的来说
- volatile仅仅用来保证该变量对所有线程的可见性,不保证原子性。
- 不要将volatile用在GetAndOperate场合。