JAVA并发编程递进篇,探索线程安全性volatile关键字如何保证可见性

一开始就直接上代码,直接来看一段木有使用volatile关键字的线程调用代码吧:php

public class VolatileDemo { public static boolean stop = false; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ int i = 0; while(!stop) { i++; //System.out.println("result:" + i); /* try { Thread.sleep(0); } catch (InterruptedException e) { e.printStackTrace(); } */ } },"myThread"); t.start(); Thread.sleep(1000); stop=true; } } 

很显然运行main()方法后,循环并无结束,程序一直处于运行状态。css

若是咱们要使得循环结束该怎么作呢?java

1、Volatile关键字的使用递进

1.1 System.out.println

使用print打印i的值,发现循环就被终止了。这是为何呢?咱们不妨来看下println()方法的源码吧。python

public void println(String x) { synchronized (this) { print(x); newLine(); } } 

底层方法使用synchronized关键字,这个同步会防止循环期间对变量stop的值缓存。缓存

从IO角度来讲**,print本质上是一个IO的操做**,咱们知道磁盘IO的效率必定要比CPU的计算效率慢得多,因此IO可使得CPU有时间去作内存刷新的事情,从而致使这个现象。好比咱们能够在里面定义一个new File()。一样会达到效果。多线程

1.2 Thread.sleep(0)

增长Thread.sleep(0)也能生效,是和cpu、以及jvm、操做系统等因素有关系。架构

官方文档上是说,Thread.sleep没有任何同步语义,编译器不须要在调用Thread.sleep以前把缓存在寄存器中的写刷新到给共享内存、也不须要在Thread.sleep以后从新加载缓存在寄存器中的值。并发

编译器能够自由选择读取stop的值一次或者屡次,这个是由编译器本身来决定的。 Thread.sleep(0)致使线程切换,线程切换会致使缓存失效从而读取到了新的值。app

1.3 Volatile关键字

public volatile static boolean stop = false; 

咱们在stop变量加上volatile关键字进行修饰,能够查看汇编指令,使用HSDIS工具进行查看。jvm

  • 在IDEA中加入VM options:
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileDemo.* 

运行程序后,在输出的结果中,查找下 lock 指令,会发现,在修改带有volatile 修饰的成员变量时,会多一个 lock 指令。

0x00000000034e49f3: lock add dword ptr [rsp],0h ;*putstatic stop ; - com.sy.sa.thread.VolatileDemo::<clinit>@1 (line 5) 0x00000000034e4643: lock add dword ptr [rsp],0h ;*putstatic stop ; - com.sy.sa.thread.VolatileDemo::<clinit>@1 (line 5) 

运行加了volatile关键字的代码,发现中多了lock汇编指令。那么lock指令是怎么保证可见性的呢?

1.3.1 什么是可见性?

在单线程的环境下,若是向一个变量先写入一个值,而后在没有写干涉的状况下读取这个变量的值,那这个时候读取到的这个变量的值应该是以前写入的那个值。这原本是一个很正常的事情。可是在多线程环境下,读和写发生在不一样的线程中的时候,可能会出现:读线程不能及时的读取到其余线程写入的最新的值。这就是所谓的可见性。

1.3.2 硬件方面了解可见性本质

硬件方面将从CPU、内存、磁盘I/O 三方面着手。

1.3.2.1 CPU的高速缓存

由于高速缓存的存在,会致使一个缓存一致性问题。 

1.3.2.2 总线锁和缓存锁

总线锁,简单来讲就是,在多cpu下,当其中一个处理器要对共享内存进行操做的时候,在总线上发出一个LOCK#信号,这个信号使得其余处理器没法经过总线来访问到共享内存中的数据,总线锁定把CPU和内存之间的通讯锁住了,这使得锁按期间,其余处理器不能操做其余内存地址的数据,因此总线锁定的开销比较大,这种机制显然是不合适的 。

如何优化呢?最好的方法就是控制锁的保护粒度,咱们只须要保证对于被多个CPU缓存的同一份数据是一致的就行。在P6架构的CPU后,引入了缓存锁,若是当前数据已经被CPU缓存了,而且是要协会到主内存中的,就能够采用缓存锁来解决问题。

所谓的缓存锁,就是指内存区域若是被缓存在处理器的缓存行中,而且在Lock期间被锁定,那么当它执行锁操做回写到内存时,再也不总线上加锁,而是修改内部的内存地址,基于缓存一致性协议来保证操做的原子性。

总线锁和缓存锁怎么选择,取决于不少因素,好比CPU是否支持、以及存在没法缓存的数据时(比较大或者快约多个缓存行的数据),必然仍是会使用总线锁。

1.3.2.3 缓存一致性

MSI ,MESI 、MOSI ... 为了达到数据访问的一致,须要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操做,常见的协议有MSI,MESI,MOSI等。最多见的就是MESI协议。接下来给你们简单讲解一下MESIMESI表示缓存行的四种状态,分别是:

  • M(Modify): 表示共享数据只缓存在当前CPU缓存中,而且是被修改状态,也就是缓存的数据和主内存中的数据不一致;
  • E(Exclusive): 表示缓存的独占状态,数据只缓存在当前CPU缓存中,而且没有被修改;
  • S(Shared): 表示数据可能被多个CPU缓存,而且各个缓存中的数据和主内存数据一致;
  • I(Invalid): 表示缓存已经失效。

1.3.2.4 MESI带来的优化

各CPU经过消息传递来更新各个缓存行的状态。在CPU中引入了Store Bufferes。  CPU0 只须要在写入共享数据时,直接把数据写入到 store bufferes 中,同时发送 invalidate 消息,而后继续去处理其余指令。 当收到其余全部CPU发送了invalidate acknowledge消息时,再将 store bufferes 中的数据数据存储至 cache line中。最后再从缓存行同步到主内存。

指令重排序

来关注下面这段代码,假设分别有两个线程,分别执行executeToCPU0和executeToCPU1,分别由两个不一样的CPU来执行。引入Store Bufferes以后,就可能出现 b==1返回true ,可是assert(a==1)返回false。不少确定会表示不理解,这种状况怎么可能成立?那接下来咱们去分析一下,写一段伪代码吧。

executeToCPU0(){
  a=1;   b=1; } executeToCPU1(){   while(b==1){     assert(a==1);  } } 

经过内存屏障禁止了指令重排序

X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)。

  • Store Memory Barrier(写屏障):告诉处理器在写屏障以前的全部已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来讲就是使得写屏障以前的指令的结果对屏障以后的读或者写是可见的
  • Load Memory Barrier(读屏障):处理器在读屏障以后的读操做,都在读屏障以后执行。配合写屏障,使得写屏障以前的内存更新对于读屏障以后的读操做是可见的
  • Full Memory Barrier(全屏障):确保屏障前的内存读写操做的结果提交到内存以后,再执行屏障后的读写操做
volatile int a=0; executeToCpu0(){   a=1;   //storeMemoryBarrier()写屏障,写入到内存   b=1;    // CPU层面的重排序   //b=1;   //a=1; } executeToCpu1(){   while(b==1){  //true     loadMemoryBarrier(); //读屏障     assert(a==1) //false  } } 

1.3.3 软件方面了解可见性本质

1.3.3.1 JMM(Java内存模型)

简单来讲,JMM定义了共享内存中多线程程序读写操做的行为规范:在虚拟机中把共享变量存储到内存以及从内存中取出共享变量的底层实现细节。经过这些规则来规范对内存的读写操做从而保证指令的正确性,解决了CPU多级缓存、处理器优化、指令重排序致使的内存访问问题,保证了并发场景下的可见性。

须要注意的是,JMM并无主动限制执行引擎使用处理器的寄存器和高速缓存来提高指令执行速度,也没主动限制编译器对于指令的重排序,也就是说在JMM这个模型之上,仍然会存在缓存一致性问题和指令重排序问题。JMM是一个抽象模型,它是创建在不一样的操做系统和硬件层面之上对问题进行了统一的抽象,而后再Java层面提供了一些高级指令,让用户选择在合适的时候去引入这些高级指令来解决可见性问题。

1.3.3.2 JMM解决可见性有序性

其实经过前面的内容分析咱们发现,致使可见性问题有两个因素,一个是高速缓存致使的可见性问题,另外一个是指令重排序。那JMM是如何解决可见性和有序性问题的呢?其实前面在分析硬件层面的内容时,已经提到过了,对于缓存一致性问题,有总线锁和缓存锁,缓存锁是基于MESI协议。而对于指令重排序,硬件层面提供了内存屏障指令。

而JMM在这个基础上提供了volatile、final等关键字,使得开发者能够在合适的时候增长相应相应的关键字来禁止高速缓存和禁止指令重排序来解决可见性和有序性问题。

1.3.3.3 Volatile底层的原理

经过javap -v VolatileDemo.class 分析汇编指令。

public static volatile boolean stop;  descriptor: Z  flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE int field_offset = cache->f2_as_index();      if (cache->is_volatile()) {       if (tos_type == itos) {        obj->release_int_field_put(field_offset, STACK_INT(-1));      } else if (tos_type == atos) {        VERIFY_OOP(STACK_OBJECT(-1));        obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));        OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);      } else if (tos_type == btos) {        obj->release_byte_field_put(field_offset, STACK_INT(-1));      } else if (tos_type == ltos) {        obj->release_long_field_put(field_offset, STACK_LONG(-1));      } else if (tos_type == ctos) {        obj->release_char_field_put(field_offset, STACK_INT(-1));      } else if (tos_type == stos) {        obj->release_short_field_put(field_offset, STACK_INT(-1));      } else if (tos_type == ftos) {        obj->release_float_field_put(field_offset, STACK_FLOAT(-1));      } else {        obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));      }       OrderAccess::storeload();     } 

1.3.4 Happens-Before模型

除了显示引用volatile关键字可以保证可见性之外,在Java中,还有不少的可见性保障的规则。

从JDK1.5开始,引入了一个happens-before的概念来阐述多个线程操做共享变量的可见性问题。因此咱们能够认为在JMM中,若是一个操做执行的结果须要对另外一个操做可见,那么这两个操做必需要存在happens-before关系。这两个操做能够是同一个线程,也能够是不一样的线程。

1.3.4.1 程序顺序规则

能够认为是as-if-serial语义。

  • 不能改变程序的执行结果(在单线程环境下,执行的结果不变)
  • 依赖问题, 若是两个指令存在依赖关系,是不容许重排序
int a=0; int b=0; void test(){   int a=1;   a   int b=1;   b   //int b=1;   //int a=1;   int c=a*b;  c } 

a happens -before b ; b happens before c

1.3.4.2 传递性规则

a happens-before b , b happens- before c, a happens-before c

1.3.4.3 volatile变量规则

  • volatile 修饰的变量的写操做,必定happens-before后续对于volatile变量的读操做.
  • 内存屏障机制来防止指令重排.
public class VolatileExample{   int a=0;   volatile boolean flag=false;   public void writer(){     a=1;             1     flag=true; //修改       2  }   public void reader(){     if(flag){ //true       3       int i=a;  //1      4    }  } } 
  • 1 happens-before 2 是否成立? 是 -> ?
  • 3 happens-before 4 是否成立? 是
  • 2 happens -before 3 ->volatile规则
  • 1 happens-before 4 ; i=1成立.

1.3.4.4 监视器锁规则

对一个锁的解锁,happens-before 于随后对这个锁的加锁

int x=10; synchronized(this){   //后续线程读取到的x的值必定12   if(x<12){     x=12;  } } x=12; 

1.3.4.5 start规则

若是线程 A 执行操做 ThreadB.start(),那么线程 A 的 ThreadB.start()操做 happens-before 线程 B 中的任意操做

public class StartDemo{   int x=0;   Thread t1=new Thread(()->{     //读取x的值 必定是20     if(x==20){          }  });   x=20;   t1.start();   } 

1.3.4.6 Join规则

若是线程 A 执行操做 ThreadB.join()并成功返回,那么线程 B 中的任意操做 happens-before 于线程A 从 ThreadB.join()操做成功返回

public class Test{   int x=0;   Thread t1=new Thread(()->{     x=200;  });   t1.start();   t1.join(); //保证结果的可见性。   //在此处读取到的x的值必定是200. }
来源:站长
相关文章
相关标签/搜索