由淺入深Java內存模型

JMM

Java內存模型描述了Java程序中各類變量(共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取這些變量的底層細節。程序員

  • 主存:全部共享變量都保存在主存中。
  • 工做內存:每一個線程都有本身獨立的工做內存,裏面保存該線程使用到的變量的副本。

兩條規定:

  • 線程對共享變量的全部操做都必須在本身的工做內存中進行,不能直接從主存中讀寫。
  • 不一樣線程的工做內存之間沒法直接相互訪問,線程之間的變量傳遞,必須經過主存來完成。

在開始併發編程時,咱們須要思考兩個關鍵的問題:1.線程之間如何通訊?2.線程之間如何同步?編程

線程通訊

在命令式編程中,線程之間有兩種通訊方式:數組

  1. 共享內存:線程之間存在公共狀態,線程間經過獨寫內存中的公共狀態來隱式進行通訊。
  2. 消息傳遞:線程之間沒有公共狀態,須要發送消息來進行顯式通訊。

線程同步

同步是指程序用於控制線程發生相對順序執行的機制。在共享內存模型裏,程序員須要給代碼加上制定的互斥操做來顯式進行;在消息傳遞模型中,通訊是對程序員透明的,是隱式進行的。緩存

可見性

全部的實例域、靜態域、數組元素是儲存在堆中的,線程之間能夠共享,能夠將它們稱爲「共享變量」,他們可能會在併發編程時出現「可見性」問題;而局部變量、方法參數、異常處理參數不會在線程之間共享,不受內存模型的影響。bash

假如一個變量被多個線程使用到,那麼這個共享變量會在多個線程的工做內存中都存在副本。多線程

當一個共享變量被一個線程修改,可以及時被其餘線程看到,這叫作可見性。併發

要實現可見性,須要保證兩點:

  • 共享變量被修改後,能及時刷新到主存中去。
  • 其餘線程能及時將主存中更新的信息刷新到本身的工做內存中。

重排序

as-if-serial語義:

  • 不管怎麼重排序,程序執行的結果必須是與未排序狀況下一致的。(Java保證在單線程狀況下遵循詞語義)
  • 多線程中程序交錯執行時,重排序可能會致使內存可見性問題。

數據依賴性

若是兩個操做訪問同一個變量,並且這兩個操做中有一個爲寫操做,那麼這兩個操做之間就存在了數據依賴性。數據依賴性存在如下三種狀況:性能

操做 示例
先寫,後讀 a=1;b=a;
先寫,後寫 a=1;a=2;
先讀,後寫 b=a;a=1;

不難發現,上面的三種狀況,只要重排序其指令,結果都會產生變化。優化

因此編譯器和處理器在進行重排序時,必須遵照數據依賴性。不能對存在數據依賴性的兩個操做進行重排序。ui

控制依賴性

看下面一段代碼

if(flag){ //操做1
    int num=a+b; //操做2
}
複製代碼

能夠看到,操做1和操做2並不存在數據依賴,可是存在控制依賴。當代碼中出現控制依賴時,會影響程序的並行度。所以,編譯器和處理器會採用一種「猜想執行」來克服控制依賴性對並行度的影響(並行是爲了效率和性能)處理器可能提早執行操做2,將a+b計算出來,並放置到一個叫「重排序緩存」的緩存中,假如得知操做1中的flag爲真,再將結果寫入num中。

在單線程程序中,對存在控制依賴關係的重排序,不會影響結果;不過在多線程中,可能會影響到結果。

指令重排序:

實際執行的代碼順序和程序員書寫的順序是不同的,編譯器或處理器爲了提升性能,在不影響程序結果的前提下,會進行執行順序的優化。

  • 編譯器優化重排序(編譯器)
  • 指令級並行重排序(處理器)
  • 內存系統重排序(處理器)

致使不可見的緣由:

  • 線程的交叉執行(原子性問題)
  • 重排序結合線程交叉執行(原子性問題)
  • 共享變量更新後的值,沒有在工做內存和主存之間獲得及時的更新。(可見性問題)

synchronized實現可見性

原子性:

經過互斥鎖來實現。

可見性:

  • 線程解鎖前,必須把共享變量的最新值刷新到主存中去。
  • 線程加鎖時,會清空當前工做內存中共享變量的值,從主存中從新獨取最新的值。

流程:

  1. 得到互斥鎖
  2. 清空工做內存
  3. 從主存拷貝共享變量的最新副本到工做內存
  4. 執行代碼
  5. 將更改後的共享變量的值刷新到主存
  6. 釋放互斥鎖

volatile實現可見性

可以保證volatile變量的可見性,可是不能保證volatile變量複合操做的原子性。 volatile經過加入內存屏障和禁止指令重排序來實現可見性的。對volatile變量執行寫操做時,會在寫入後加一條store的屏障指令;對volatile變量執行讀操做時,會在讀操做前加入一條load屏障指令。

處理器級別的重排序與內存屏障指令

因爲處理器的速度很快,爲了不處理器停頓下來等待內存(內存確定跟不上處理器的速度)而產生的延遲,現代的處理器使用緩存區來臨時保存處理器向內存寫入的數據,而後再提供給內存,以保證接二連三地高效運行。

雖然緩存區存在諸多好處,可是它是僅對處理器可見的。這將會產生一個重要的問題:處理器堆內存的獨寫操做的順序,可能與內存中實際發生讀寫操做順序不一致。(由於現代的處理器大都容許使用重排序)

因此,爲了保證可見性,Java編譯器會在生成指令序列時,插入內存屏障來禁止特定的處理器進行重排序。

線程寫入volatile變量的過程:

  1. 改變線程工做內存中volatile變量副本的值
  2. 將改變後的副本的值從工做內存刷新到主存

線程讀volatile變量的過程:

  1. 從主存中獨取volatile變量的最新的值到工做內存中。
  2. 從工做內存中獨取變量的副本

volatile不能保證volatile變量符合操做的原子性:

舉一個例子:

public class VolatileDemo{
    private volatile int num=0;
    public int getNumber(){
        return this.num;
    }
    public void increase(){
        this.num++;
    }
    
    public static void main(String[] args){
        final VolatileDemo v=new VolatileDemo();
        for(int i;i<500;i++){
            new Thread(new Runnable(){
            
            public void run(){
                v.increase();
            }
            }).start();
        }
        
        //主線程主動讓出資源讓500個子線程運行。這個‘1’指的是主線程
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println(v.getNumber());
    }
}
複製代碼

這個程序是開啓500個線程,每一個線程執行一次increase()操做,給變量num加一。運行這個程序屢次,發現並非每次輸出結果都是500。

發生了什麼問題?

由於this.num++這條語句,實際上是三步操做,不具有原子性。假設一個運行場景:

  1. 此時num=1
  2. 線程A讀取num爲1,A工做內存中num=1
  3. 線程B獨取num爲1,B工做內存中num=1
  4. 線程B進行加1操做,寫入B工做內存,B工做內存中num=2,更新到主存,主存中num=2.
  5. 線程A進行加1操做,寫入A工做內存,A工做內存中num=2, 更新到主存,主存中num=2.

可見,進行了兩次加1操做,可是主存中的num只增長了1。怎麼解決呢?咱們要保證num自增操做的原子性。

volatile注意事項

  1. 對變量的寫入操做不能依賴當前值。
  2. 該變量沒有包含在具備其餘變量的不變式中。

二者比較

  1. volatile 不須要加鎖,比synchronized更輕量級,不會阻塞線程。
  2. 從可見性角度講,volatile讀至關於加鎖,寫至關於解鎖。(前文提到的屏障指令)
  3. synchronized能夠保證原子性和可見性,volatile只保證了可見性。

相關文章
相關標籤/搜索