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. }
來源:站長
相關文章
相關標籤/搜索