多線程之美1一volatile

目錄
1、java內存模型
1.一、抽象結構圖
1.二、概念介紹
2、volatile詳解
2.一、概念
2.二、保證內存可見性
2.三、不保證原子性
2.四、有序性java

1、java內存模型

1.一、抽象結構圖

1.二、概念介紹

  • java 內存模型程序員

    即Java memory model(簡稱JMM), java線程之間的通訊由JMM控制,決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。
  • 多線程通訊一般分爲2類:共享內存和消息傳遞編程

    JMM採用的就是共享內存來實現線程間的通訊,且通訊是隱式的,對程序開發人員是透明的,因此在瞭解其原理了,纔會對線程之間通訊,同步,內存可見性問題有進一步認識,避免開發中出錯。
  • 線程之間如何通訊?數組

    在java中多個線程之間要想通訊,如上圖所示,每一個線程在須要操做某個共享變量時,會將該主內存中這個共享變量拷貝一份副本存在在本身的本地內存(也叫工做內存,這裏只是JMM的一個抽象概念,即將其籠統看作一片內存區域,用於每一個線程存放變量,實際涉及到緩存,寄存器和其餘硬件),線程操做這個副本,好比 int i = 1;一個線程想要進行 i++操做,會先將變量 i =1 的值先拷貝到本身本地內存操做,完成 i++,結果 i=2,此時主內存中的值仍是1,在線程將結果刷新到主內存後,主內存值就更新爲2,數據達到一致了。
    
    若是線程A,線程B同時將 主內存中 i =1拷貝副本到本身本地內存,線程A想要 將i+1,而線程B想要將 int j=i,將賦值給j,那麼如何保證線程之間的協做,此時就會涉及到線程之間的同步以及內存可見性問題了。(後文分析synchronized/lock)
     那線程之間實現通訊須要通過2個步驟,藉助主內存爲中間媒介:
       線程A (發送消息)-->(接收消息) 線程B  
       一、線程A將本地內存共享變量值刷新到主內存中,更新值;
       二、線程B從主內存中讀取已更新過的共享變量;
  • 共享內存中涉及到哪些變量稱爲共享變量?緩存

    這裏的共享內存指的是jvm中堆內存中,全部堆內存在線程之間共享,由於棧中存儲的是方法及其內部的局部變量,不在此涉及。
    共享變量:對於多線程之間可以共同操做的變量,包含實例域,靜態域,數組元素。即有成員變量,靜態變量等等,
       不涉及到局部變量(因此局部變量不涉及到內存可見性問題)
  • 多線程在java內存模型中涉及到三個問題多線程

    • 可見性
    • 原子性
    • 有序性(涉及指令重排序)

2、volatile詳解

2.一、概念

-一、volatile 是 java中的關鍵字,可修飾字段,能夠保證共享變量的在內存的可見性,有序性,不保證原子性。
-二、做用:在瞭解java內存模型後,才能更加了解volatile在JMM中的做用,volatile在JMM中爲了保證內存的可見性,便是線程之間操做共享變量的可見性。
  • volatile寫和讀的內存語義
volatile 寫的內存語義:
    當寫一個volatile修飾的共享變量時,JMM會把該線程的本地內存的共享變量副本值刷新到主內存中;
volatile 讀的內存語義:
    當讀一個volatile修飾的共享變量時,JMM會將該線程的本地內存的共享變量副本置爲無效,要求線程從新去主內存中獲取最新的值。
  • java內存模型控制與volatile衝突嗎?什麼區別?
不衝突!java內存模型控制線程工做內存與主內存之間共享變量會同步,即線程從主內存中讀一份副本到工做內存,又刷新到主內存,那怎麼還須要 volatile來保證可見性,不是JMM本身能控制嗎,通常狀況下JMM能夠控制 2分內存數據一致性,可是在多線程併發環境下,雖然最終線程工做內存中的共享變量會同步到主內存,但這須要時間和觸發條件,線程之間同時操做共享變量協做時,就須要保證每次都能獲取到主內存的最新數據,保證看到的工做變量是最後一次修改後的值,這個JMM無法控制保證,這就須要volatile或者後文要講的 synchronized和鎖的同步機制來實現了。

2.二、保證內存可見性

  • 一、多個線程出現內存不可見問題示例併發

    /**
     * @author zdd
     * Description: 測試線程之間,內存不可見問題
     */
    public class TestVisibilityMain {
        private static boolean isRunning = true;
    
      // 可嘗試添加volatile執行,其他不變,查看線程A是否被中止
      //private static volatile boolean isRunning = true;
    
        public static void main(String[] args) throws InterruptedException {
    //1,開啓線程A,讀取共享變量值 isRunning,默認爲true 
            new Thread(()->{
              // --> 此處用的lamda表達式,{}內至關於Thread的run方法內部需執行任務 
                System.out.println(Thread.currentThread().getName() + "進入run方法");
                while (isRunning == true) {
                }
                System.out.println(Thread.currentThread().getName()+"被中止!");
            },"A").start();
            //2,主線程休眠1s, 確保線程A先被調度執行
            TimeUnit.SECONDS.sleep(1);
        //3,主線程修改共享變量值 爲flase,驗證線程A是否可以獲取到最新值,跳出while循環  --> 驗證可見性
            isRunning =false;
            System.out.println(Thread.currentThread().getName() +"修改isRunning爲: " + isRunning);
        }
    }

​ 執行結果以下圖:jvm

  • 二、一個容易忽視的問題
上面代碼 while裏面是一個空循環,沒有操做,若是我在裏面加一句打印語句,線程A會被中止,這是怎麼回事呢?
 原:while (isRunning == true) {}
 改1:
 while (isRunning == true) {
     System.out.println("進入循環");
 }
原來 println方法裏面加了 synchronized關鍵字,在加了鎖既保證原子性,也保證了可見性,會實現線程的工做內存與主內存共享變量的同步。
源代碼以下:
 public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
  改2:
  while (isRunning == true) {
       //改成這樣,也能夠中止線程A
                synchronized (TestVisibilityMain.class){}
   }

2.三、不保證原子性

  • 一、示例代碼
/**
 * @author zdd
 * Description: 測試volatile的不具備原子性
 */
public class TestVolatileAtomic {

    private static volatile   int  number;
    //開啓線程數
    private static final  int THREAD_COUNT =10;
    //執行 +1 操做
    public static void  increment() {
       //讓每一個線程進行加1次數大一些,可以更容易出現volatile對複合操做(i++)沒有原子性的錯誤
        for (int i = 0; i < 10000; i++) {
          number++;
        }
    }

    public static int getNumber() {
        return number;
    }

    public static void main(String[] args) throws InterruptedException {
        TestVolatileAtomic volatileAtomic  = new TestVolatileAtomic();
        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i]=
            new Thread(()->{
               // 作循環自增操做
                volatileAtomic.increment();
                System.out.println(Thread.currentThread().getName() +"的number值: "+volatileAtomic.getNumber());
            },"thread-"+i);
        }

        for (int i = 0; i <10; i++) {
          //開啓線程
            threads[i].start();
        }
        //主線程休眠4s,確保上面線程都執行完畢
        TimeUnit.SECONDS.sleep(4);
        System.out.println("執行完畢,number最終值爲:"+volatileAtomic.getNumber());
    }
}

執行結果:number的最後值不必定是 10*10000= 100000的結果
  • 二、緣由分析
對單個volatile變量的讀/寫具備原子性,而對像 i++這種複合操做不具備原子性。
上面代碼 i++操做能夠分爲3個步驟
-1 先讀取變量i的值   i
-2 進行i+1操做   temp= i+1
-3 修改i的值     i= temp
好比:好比在線程A,B同時去操做共享變量i, i的初始值爲10,A,B同時去獲取i的值,A對i進行 temp =i+1,此時i的值還沒變, 線程B也對i進行 temp=i+1了,線程A執行i=temp的操做,i的值變爲11,此時因爲 volatile可見性,會刷新A的 i值到主內存,主內存中i此時也更新爲11了,線程B接收到通知本身i無效了,從新讀取i=11,雖然i=11,可是已經進行過 temp= i+1了,此時temp =11,線程B繼續第三步,i=temp =11, 預期結果是i被A,B自增各一次,結果i=12,如今爲11,出現數據錯誤。

2.四、有序性

  • 重排序
-1,重排序概念:重排序是編譯器和處理器爲了優化程序性能而對指令序列從新排序的一種手段
即:程序員編寫的程序代碼的順序,在實際執行的時候是不同的,這其中編譯器和處理器在不影響最終執行結果的基礎上會作一些優化調整,有從新排序的操做,爲了提升程序執行的併發性能。
-2,重排序分類: 編譯重排序,處理器重排序
-4,單線程下,重排序沒有問題,可是在多線程環境下,可能會破壞程序的語義.
  • volatile 防止重排序保證有序性

爲了實現volatile的內存語義,JMM會限制編譯器和處理器重排序性能

-1 制定了重排序規則表編譯器防止編譯器重排序測試

volatile重排序規則表(圖摘自書-併發編程的藝術)

-2 插入內存屏障防止處理器重排序

參考資料: 一、Java併發編程的藝術- 方騰飛 二、java多線程編程核心技術- 高洪巖

相關文章
相關標籤/搜索