Java 多線程 相關概念

前言

本篇文章介紹一些多線程的相關的深刻概念。理解後對於線程的安全性會有更深的理解。html

先說一個格言,摘自Java核心技術:
若是向一個變量寫入值,而這個變量接下來可能會被另外一個線程讀取;或者一個變量讀值,而這個變量多是以前被另外一個線程寫入的,此時必須同步。java

下面就是概念了。程序員

1. Monitor機制:

  • Monitor實際上是一種同步工具、同步機制,一般被描述成一個對象,主要特色是:編程

    1. 同步。
      對象的全部方法都被互斥的執行。比如一個Monitor只有一個運行「許可」,任一個線程進入任何一個方法都須要得到這個「許可」,離開時把許可歸還。
    2. 協做。
      一般提供signal機制。容許正持有許可的線程暫時放棄許可,等待某個監視條件成真,條件成立後,當前線程能夠通知正在等待這個條件的線程,讓它能夠從新得到運行許可。
  • 在 Monitor Object 模式中,主要有四種類型參與者:數組

    1. 監視者對象 Monitor Object
      負責公共的接口方法,這些公共的接口方法會在多線程的環境下被調用執行。
    2. 同步方法
      這些方法是監視者對象所定義。爲了防止競爭條件,不管是否有多個線程併發調用同步方法,仍是監視者對象還用多個同步方法,在任一事件內只有一個同步方法可以執行。
    3. 監控鎖 Monitor Lock
      每個監視者對象都會擁有一把監視鎖。
    4. 監控條件 Monitor Condition
      同步方法使用監視鎖和監視條件來決定方法是否須要阻塞或從新執行。
  • Java中,Object 類自己就是監視者對象,Java 對於 Monitor Object 模式作了內建的支持。緩存

    • Object 類自己就是監視者對象
    • 每一個 Object 都帶了一把看不見的鎖,一般叫 內部鎖/Monitor 鎖/Instrinsic Lock, 這把鎖就是 監控鎖
    • synchronized 關鍵字修飾方法和代碼塊就是同步方法
    • wait()/notify()/notifyAll() 方法構成監控條件(Monitor Condition)

2. 內存模型

Java的併發採用的是共享內存模型,線程間通訊是隱式的,同步是顯示的;而咱們在Android中所常說的Handler通訊即採用的是消息傳遞模型,通訊是顯示的,同步是隱式的。安全

  • 併發編程模型的分類
    併發編程中,須要處理兩個問題:線程之間如何通訊、線程之間如何同步。bash

    • 通訊是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通訊機制有兩種:共享內存和消息傳遞。
      在共享內存的併發模型裏,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊;而在消息傳遞模型裏,線程之間沒有公共狀態,必須經過明確的發送信息來顯示進行通訊。
    • 同步是指程序用於控制不一樣線程之間操做發生相對順序的機制。
      在共享內存併發模型裏,同步是顯示進行的,程序員必須顯示指定某段代碼或方法須要在線程間互斥執行;而在消息傳遞模型中,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。
  • Java內存模型的抽象
    Java堆內存在線程間共享,下文所說的共享變量即被存儲在堆內存中變量:實例域、靜態域和數組。局部變量、方法定義參數和異常處理參數不會在線程之間共享,不會有內存可見性問題,也不受內存模型影響。多線程

  • Java線程之間的通訊由Java內存模型(JMM,Java Memory Module)控制,JMM決定了一個線程對共享變量的寫入什麼時候對另外一個線程可見。
    JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存中,每一個線程都有一個私有的本地內存,也叫工做內存,本地內存中存儲了該線程以讀/寫共享變量的副本。(本地內存是JMM的一個抽象概念,並不真實存在,它涵蓋了緩存、寫緩衝區、寄存器以及其餘的硬件和編譯器優化。)
    因此線程A和線程B要通訊步驟以下:併發

    1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去
    2. 而後,線程B到主內存中去讀取線程A以前已更新過的共享變量
  • 線程模型圖

    線程模型
    線程模型

3. 原子性

原子性指:一個操做(有可能包含有多個子操做)要麼所有執行(生效),要麼所有都不執行(都不生效)。
java.util.concurrent.atomic包中不少類使用了CAS指令來保證原子性,而再也不使用鎖。如AtomicIntergerAtomicBooleanAtomicLongAtomicReference等。
原子性不保證順序一致性,只保證操做是原子的。

4. 內存可見性

可見性是指,當多個線程併發訪問共享變量時,一個線程對共享變量的修改,其它線程可以當即看到。

  • 從上面可知道線程模型,線程a對共享變量修改時先把值放到本身的工做內存中,而後再把工做內存中的共享變量更新到主內存中;線程b一樣如此;當線程a更新了主內存後線程b刷新工做內存後就能看到a更新後的最新值。這就是內存可見性問題。
  • 內存可見性要保證兩點:
    1. 線程修改後的共享變量更新到主內存;
    2. 從主內存中更新最新值到工做內存中;

5. happens-before

happens-before規則對應於一個或多個編譯器和處理器重排序規則,對於程序員來講,該規則易懂,避免爲了理解JMM提供的內存可見性保證而去學習複雜的重排序規則以及這些規則的具體實現。

使用happens-before的概念來闡述操做之間的內存可見性
若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必需要存在happens-before關係。
這兩個操做能夠在一個線程內,也能夠是不一樣線程。

兩個操做之間具備happens-before關係,並不意味着前一個操做必需要在後一個操做前執行;僅僅要求前一個操做的執行結果對後一個可見,且前一個操做按順序排在第二個操做以前。

  • 傳遞規則:若是操做1在操做2前面,而操做2在操做3前面,則操做1確定會在操做3前發生。該規則說明了happens-before原則具備傳遞性
  • 鎖定規則:一個unlock操做確定會在後面對同一個鎖的lock操做前發生。這個很好理解,鎖只有被釋放了纔會被再次獲取
  • volatile變量規則:對一個被volatile修飾的寫操做先發生於後面對該變量的讀操做
  • 程序次序規則:一個線程內,按照代碼順序執行
  • 線程啓動規則:Thread對象的start()方法先發生於此線程的其它動做
  • 線程終結原則:線程的終止檢測後發生於線程中其它的全部操做
  • 線程中斷規則: 對線程interrupt()方法的調用先發生於對該中斷異常的獲取
  • 對象終結規則:一個對象構造先於它的finalize發生

6. CAS指令

是現代處理器上提供的高效機器級別的原子指令,這些原子指令以原子方式對內存執行讀-寫-改操做,這是在多處理器中實現同步的關鍵。
AtomicIntergerAtomicBooleanAtomicLong的實現都是基於CAS指令。

7. 重排序

在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,儘量的提升開發並行度。

  • 編譯器和處理器會對指令進行重排序以提升性能,重排序有三種類型:
    1. 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
    2. 指令級別的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。若是不存在數據依賴,處理器能夠改變語句對應機器指令的執行順序。
    3. 內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是亂序執行。
  • 這些重排序均可能會致使多線程程序出現內存可見性問題。
    對於處理器重排序,JMM會要求編譯器在生成指令序列時插入特定類型的內存屏障指令來禁止特定類型的處理器重排。
    JMM屬於語言級別的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺上,經過禁止一些重排序問題來保證內存可見性。
  • as-if-serial語義
    是指無論怎麼重排序,單線程程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵照as-if-serial語義。
    因此,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。

  • 數據依賴性

    • 有三種類型:
      1. 寫後讀:a = 1; b = a;
      2. 寫後寫:a = 1; a = 2;
      3. 讀後寫:a = b; b =1;
    • 舉個例子:
      int a = 1; int b = 1; int sum = a + b;
      A和B不存在數據依賴,sum卻依賴A和B。因此執行順序多是ABsum,也多是BAsum。
  • 重排序對多線程的影響
    重排序破壞了多線程程序的語義。對於存在控制依賴的操做(if語句)進行重排序,由於單線程程序是按順序來執行的,因此執行結果不會改變;而多線程程序中,重排序可能會改變運行結果。
    對控制依賴if(flag){b = a*a}的重排序以下,編譯器和處理器會採用猜想執行來克服相關性來對並行度的影響,對先提取並計算a*a,而後把計算結果保存到名爲重排序緩衝的硬件緩存中,接下來再判斷flag是否爲真。另外一個線程設置爲true了,並設置a=1,然而取得的值可能爲0,與預期不符。這就是影響的一個案例。

  • 重排序的一個示例,摘自EffectiveJava:

    while(!done) {
      i++
    }
    //重排後。這種優化稱做提示,是HopSpot Server VM的工做
    if(!done){
      while(true) {
        i++;
      }
    }複製代碼

8. 順序一致性

若是一個多線程程序能正確同步,這個程序將是一個沒有數據競爭的程序。JMM對正確同步的多線程程序的內存一致性作了以下保證:若是程序是正確同步的,程序的執行將具備順序一致性,即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。

  • 順序一致性內存模型(爲程序員提供了極強的內存可見性保證)的兩大特性:
    1. 一個線程中的全部操做必須按照程序的順序來執行
    2. 全部線程都只能看到一個單一的操做執行順序。每一個操做都必須原子執行且馬上對全部線程可見。
  • 其中對順序一致性和原子性的區別
    原子性保證操做的原子性,而不是順序的一致性。

9. volatile域

首先要明確,線程的安全性須要三點保證:原子性、可見性,順序性。只有知足了這三個條件時線程纔是安全的。

  • 一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾以後,那麼就具有了兩層語義:
    1. 保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。
      volatile 變量保證的是一個線程對它的寫會當即刷新到主內存中,並置其它線程的副本爲無效,它並不保證對 volatile 變量的操做都是具備原子性的。
    2. 禁止進行指令重排序。
  • synchronized、Lock徹底保證了這三點;volatile僅保證了可見性和順序性(禁止指令重排),在某些狀況下可使用volatile代替synchronized以提升性能。在這種狀況下,volatile是輕量級的synchronized。

    • 某些狀況下是指:
      假設對共享變量除了賦值之外並不完成其餘操做,那麼能夠將這些共享變量聲明爲volatile。即共享變量自己的操做是原子性的、順序性的,只缺可見性了,此時能夠用volatile關鍵字。在使用時要仔細分析。
      具體是指:

      • 對變量的寫操做不依賴於當前值。
      • 該變量沒有包含在具備其餘變量的不變式中。
    • 要記住,原子性指的是對共享變量的操做(包括其子操做,即多條語句)是一塊的,要麼執行,要麼不執行。不是說用了AtomicInteger就是原子性的,而是對AtomicInteger這個共享變量的操做是否是多條語句,這些多條語句是否是原子性的。

  • 經典示例1:單例模式

  • 經典示例2:

    boolean volatile isRunning = false;
    public void start () {
    new Thread( () -> {
      while(isRunning) {
        someOperation();
      }
    }).start();
    }
    public void stop () {
    isRunning = false;//只有賦值操做,非多條語句
    }複製代碼


參考:
Java進階(二)當咱們說線程安全時,到底在說什麼
Java併發編程:volatile關鍵字解析
併發模型——共享內存模型(線程與鎖)理論篇
《深刻理解Java內存模型》

相關文章
相關標籤/搜索