Week 1 - Java 多線程 - Java 內存模型

前言

學習狀況記錄java

  • 時間:week 1
  • SMART子目標 :Java 多線程

學習Java多線程,要了解多線程可能出現的並發現象,瞭解Java內存模型的知識是必不可少的。編程

對學習到的重要知識點進行的記錄。緩存

注:這裏提到的是Java內存模型,是和併發編程相關的,不是JVM內存結構(堆、方法棧這些概念),這兩個不是一回事,別弄混了。安全

Java 內存模型

Java內存模型(Java Memory Model ,JMM)就是一種符合內存模型規範的,屏蔽了各類硬件和操做系統的訪問差別的,保證了Java程序在各類平臺下對內存的訪問都能獲得一致效果的機制及規範。目的是解決因爲多線程經過共享內存進行通訊時,存在的原子性、可見性(緩存一致性)以及有序性問題。

主內存與工做內存

先看計算機硬件的緩存訪問操做:多線程

​ 處理器上的寄存器的讀寫的速度比內存快幾個數量級,爲了解決這種速度矛盾,在它們之間加入了高速緩存。併發

​ 加入高速緩存帶來了一個新的問題:緩存一致性。若是多個緩存共享同一塊主內存區域,那麼多個緩存的數據可能會不一致,須要一些協議來解決這個問題。函數

Java的內存訪問操做與上述的硬件緩存具備很高的可比性:post

​ Java內存模型中,規定了全部的變量都存儲在主內存中,每一個線程還有本身的工做內存,工做內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。線程只能直接操做工做內存中的變量,不一樣線程之間的變量值傳遞須要經過主內存來完成。學習

內存間交互操做

Java 內存模型定義了 8 個操做來完成主內存和工做內存的交互操做this

  • read:把一個變量的值從主內存傳輸到線程的工做內存中
  • load:在 read 以後執行,把 read 獲得的值放入線程的工做內存的變量副本中
  • use:把線程的工做內存中一個變量的值傳遞給執行引擎
  • assign:把一個從執行引擎接收到的值賦給工做內存的變量
  • store:把工做內存的一個變量的值傳送到主內存中
  • write:在 store 以後執行,把 store 獲得的值放入主內存的變量中
  • lock:做用於主內存的變量,把一個變量標識成一條線程獨佔的狀態
  • unlock: 做用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。

內存模型三大特性

原子性

Java 內存模型保證了 readloaduseassignstorewritelockunlock 操做具備原子性,例如對一個 int 類型的變量執行 assign 賦值操做,這個操做就是原子性的。可是 Java 內存模型容許虛擬機將沒有被 volatile 修飾的 64 位數據(longdouble)的讀寫操做劃分爲兩次 32 位的操做來進行,也就是說基本數據類型的訪問讀寫是原子性的,除了longdouble是非原子性的,loadstoreread write 操做能夠不具有原子性。書上提醒咱們只須要知道有這麼一回事,由於這個是幾乎不可能存在的例外狀況。

雖然上面說對基本數據類型的訪問讀寫是原子性的,可是不表明在多線程環境中,如int類型的變量不會出現線程安全問題。詳細的例子能夠參考範例一

想要保證原子性,能夠嘗試如下幾種方式:

  • 若是是基礎類型的變量的話,使用Atomic類(例如AtomicInteger)
  • 其餘狀況下,可使用synchronized互斥鎖來保證 限定臨界區 內操做的原子性。它對應的內存間交互操做爲:lock 和 unlock,在虛擬機實現上對應的字節碼指令爲 monitorenter 和 monitorexit

可見性

可見性指的是,當一個線程修改了共享變量中的值,其餘線程可以當即得知這個修改。Java 內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。

可見性的錯誤問題範例比較難以模擬,有興趣的能夠藉助此篇文章更好的理解。

想要保證可見性,主要有三種實現方式:

  • volatile

    • Java的內存分主內存和線程工做內存,volatile保證修改當即由當前線程工做內存同步到主內存,但其餘線程仍須要從主內存取才能保證線程同步。
  • synchronized

    • 當線程獲取鎖時會從主內存中獲取共享變量的最新值,釋放鎖的時候會將共享變量同步到主內存中。最多隻有一個線程能持有鎖。
  • final

    • 被 final 關鍵字修飾的字段在構造器中一旦初始化完成,而且沒有發生 this 逃逸(其它線程經過 this 引用訪問到初始化了一半的對象),那麼其它線程就能看見 final 字段的值。

範例一中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問題,由於 volatile 並不能保證操做的原子性。

有序性

有序性是指:在本線程內觀察,全部操做都是有序的。在一個線程觀察另外一個線程,全部操做都是無序的,無序是由於發生了指令重排序。在 Java 內存模型中,容許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。

想要保證可見性,主要如下實現方式:

  • volatile

    • volatile的真正意義在於產生內存屏障,禁止指令重排序。即重排序時不能把後面的指令放到內存屏障以前。
  • synchronized

    • 它保證每一個時刻只有一個線程執行同步代碼,至關因而讓線程順序執行同步代碼。

有序性這塊比較難比較深的內容其實是指令重排序這塊的知識。我這就借花獻佛,引一篇我認爲講的比較清楚的文章。內存模型之重排序

先行發生原則

JVM 內存模型下,規定了先行發生原則,讓一個操做無需任何同步器協助就能先於另外一個操做完成。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對他們隨意的進行重排序。

  • 單一線程規則 - Single Thread Rule

    • 在其餘書上又叫 Program Order Rule - 程序次序規則
    • 在一個線程中, 在線程前面的操做先行發生於後面的操做。(準確的來講,是控制流順序,而不是代碼順序,由於或有邏輯判斷分支)
  • 管道鎖定規則 - Monitor Lock Rule

    • 一個 unlock 操做先行發生於後面對同一個鎖的 lock 操做。
  • volatile 變量規則 - Volatile Variable Rule

    • 對一個volatile 變量的寫操做先行發生於 後面對這個變量的讀操做
  • 線程啓動規則 - Thread Start Rule

    • Thread 對象的 start() 方法調用先行發生於此線程的每個動做。
  • 線程加入規則 - Thread Join Rule

    • Thread 對象的結束先行發生於 join() 方法返回。
  • 線程中斷規則 - Thread Interruption Rule

    • 對線程 interrupt() 方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過 interrupted() 方法檢測到是否有中斷髮生。
  • 對象終結規則- Finalizer Rule

    • 一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize() 方法的開始。
  • 傳遞性 - Transitivity

    • 若是操做 A 先行發生於操做 B,操做 B 先行發生於操做 C,那麼操做 A 先行發生於操做 C。

在多線程狀況下,時間前後順序和先行發生原則之間基本沒有太大的關係,咱們衡量併發安全問題的時候不要受到時間順序的告饒,一切必須以先行發生原則爲準。

插入案例幫助理解

案例一

代碼

/**
 * 內存模型三大特性 - 原子性驗證對比
 *
 * @author Richard_yyf
 * @version 1.0 2019/7/2
 */
public class AtomicExample {

    private static AtomicInteger atomicCount = new AtomicInteger();

    private static int count = 0;

    private static void add() {
        atomicCount.incrementAndGet();
        count++;
    }

    public static void main(String[] args) {
        final int threadSize = 1000;
        final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < threadSize; i++) {
            executor.execute(() -> {
                add();
                countDownLatch.countDown();
            });
        }
        System.out.println("atomicCount: " + atomicCount);
        System.out.println("count: " + count);

        ThreadPoolUtil.tryReleasePool(executor);
    }
}

Outout

atomicCount: 1000
count: 997

分析

能夠藉助下圖幫助理解。

count++這個簡單的操做根據上面的原理分析,能夠知道內存操做實際分爲讀寫存三步;由於讀寫存這個總體的操做,不具有原子性,count被兩個或多個線程讀入了一樣的舊值,讀到線程內存當中,再進行寫操做,再存回去,那麼就可能出現主內存被重複set同一個值的狀況,如上圖所示,兩個線程進行了count++,實際上只進行了一次有效操做。

案例二

代碼

class Foo {
    private int x = 100;

    public int getX() {
        return x;
    } 

    public int fix(int y) {
        x = x - y; 
        return x;
    } 
}


 public class MyRunnable implements Runnable {
    private Foo foo =new Foo(); 

    public static void main(String[] args) {
        MyRunnable r = new MyRunnable();
        Thread ta = new Thread(r,"Thread-A"); 
        Thread tb = new Thread(r,"Thread-B"); 
        ta.start(); 
        tb.start(); 
    } 

    public  void run() {
        
        for (int i = 0; i < 3; i++) {
            this.fix(30);
            try {
                Thread.sleep(1); 
            } catch (InterruptedException e) {
                e.printStackTrace(); 
            } 
            System.out.println(Thread.currentThread().getName() + " :當前foo對象的x值= " + foo.getX());
        } 
    } 

    public int fix(int y) {
        return foo.fix(y);
    } 
}

Output

Thread-A:當前foo對象的的x值= 70
Thread-B:當前foo對象的的x值= 70
Thread-A:當前foo對象的的x值= 10
Thread-B:當前foo對象的的x值= 10
Thread-A:當前foo對象的的x值= -50
Thread-B:當前foo對象的的x值= -50

分析

這個案例是案例一的變體,只是代碼有點複雜有點繞而已,實際上就是存在兩個線程,對一個實例的共享變量進行-30的操做。

read 的操做發生在x-y的x處,至關於兩個線程第一次fix(30)的時候,對x變量作了兩次100-30的賦值操做。

案例三

public class Test {
       // 是不是原子性?
    int i = 1;
    public static void main(String[] args) {
        Test test = new Test();
    }
}

請問上述 int i = 1是不是原子性的呢?

實際上很微妙。

本案例中的int a = 1在java中叫顯式初始化,它實際上包含兩次賦值,第一次java自動將a初始化爲0,第二次再賦值爲1。從這個角度看,這條語句包含了兩步操做,並非原子的。

可是因爲這句代碼是在構造方法中,而從類的實例化角度看,通常認爲構造方法中對當前實例的初始化過程是原子的。這是由於在實例化完成以前,通常是沒法從別的代碼中訪問到當前實例的。因此從這個角度看,int a = 1其實是原子的。

參考

  1. 《深刻理解Java虛擬機》
  2. https://juejin.im/post/5bd971...
  3. http://ifeve.com/concurrency-...
相關文章
相關標籤/搜索