Java 多線程 相關概念
前言
本篇文章介紹一些多線程的相關的深入概念。理解后對于線程的安全性會有更深的理解。
先說一個格言,摘自Java核心技術:
如果向一個變量寫入值,而這個變量接下來可能會被另一個線程讀取;或者一個變量讀值,而這個變量可能是之前被另一個線程寫入的,此時必須同步。
下面就是概念了。
1. Monitor機制:
-
Monitor其實是一種同步工具、同步機制,通常被描述成一個對象,主要特點是:
- 同步。
對象的所有方法都被 互斥 的執行。好比一個Monitor只有一個運行“許可”,任一個線程進入任何一個方法都需要獲得這個“許可”,離開時把許可歸還。 - 協作。
通常提供signal機制。允許正持有許可的線程暫時放棄許可,等待某個監視條件成真,條件成立后,當前線程可以通知正在等待這個條件的線程,讓它可以重新獲得運行許可。
- 同步。
-
在 Monitor Object 模式中,主要有四種類型參與者:
- 監視者對象 Monitor Object
負責公共的接口方法,這些公共的接口方法會在多線程的環境下被調用執行。 - 同步方法
這些方法是監視者對象所定義。為了防止競爭條件,無論是否有多個線程并發調用同步方法,還是監視者對象還用多個同步方法,在任一事件內只有一個同步方法能夠執行。 - 監控鎖 Monitor Lock
每一個監視者對象都會擁有一把監視鎖。 - 監控條件 Monitor Condition
同步方法使用監視鎖和監視條件來決定方法是否需要阻塞或重新執行。
- 監視者對象 Monitor Object
-
Java中,Object 類本身就是監視者對象,Java 對于 Monitor Object 模式做了內建的支持。
- Object 類本身就是監視者對象
- 每個 Object 都帶了一把看不見的鎖,通常叫 內部鎖/Monitor 鎖/Instrinsic Lock, 這把鎖就是 監控鎖
- synchronized 關鍵字修飾方法和代碼塊就是同步方法
- wait()/notify()/notifyAll() 方法構成監控條件(Monitor Condition)
2. 內存模型
Java的并發采用的是共享內存模型,線程間通信是隱式的,同步是顯示的;而我們在Android中所常說的Handler通信即采用的是消息傳遞模型,通信是顯示的,同步是隱式的。
-
并發編程模型的分類
并發編程中,需要處理兩個問題:線程之間如何通信、線程之間如何同步。
- 通信是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。
在共享內存的并發模型里,線程之間通過寫-讀內存中的公共狀態來隱式進行通信;而在消息傳遞模型里,線程之間沒有公共狀態,必須通過明確的發送信息來顯示進行通信。 - 同步是指程序用于控制不同線程之間操作發生相對順序的機制。
在共享內存并發模型里,同步是顯示進行的,程序員必須顯示指定某段代碼或方法需要在線程間互斥執行;而在消息傳遞模型中,由于消息的發送必須在消息的接收之前,因此同步是隱式進行的。
- 通信是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。
-
Java內存模型的抽象
Java堆內存在線程間共享,下文所說的共享變量即被存儲在堆內存中變量:實例域、靜態域和數組。局部變量、方法定義參數和異常處理參數不會在線程之間共享,不會有內存可見性問題,也不受內存模型影響。
-
Java線程之間的通信由Java內存模型(JMM,Java Memory Module)控制,JMM決定了一個線程對共享變量的寫入何時對另一個線程可見。
JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,也叫工作內存,本地內存中存儲了該線程以讀/寫共享變量的副本。(本地內存是JMM的一個抽象概念,并不真實存在,它涵蓋了緩存、寫緩沖區、寄存器以及其他的硬件和編譯器優化。)
所以線程A和線程B要通信步驟如下:
- 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去
- 然后,線程B到主內存中去讀取線程A之前已更新過的共享變量 </ol> </li>
-
線程模型圖
線程模型
</ul>
- 從上面可知道線程模型,線程a對共享變量修改時先把值放到自己的工作內存中,然后再把工作內存中的共享變量更新到主內存中;線程b同樣如此;當線程a更新了主內存后線程b刷新工作內存后就能看到a更新后的最新值。這就是內存可見性問題。
- 內存可見性要保證兩點:
- 線程修改后的共享變量更新到主內存;
- 從主內存中更新最新值到工作內存中;
- 傳遞規則:如果操作1在操作2前面,而操作2在操作3前面,則操作1肯定會在操作3前發生。該規則說明了happens-before原則具有傳遞性
- 鎖定規則:一個unlock操作肯定會在后面對同一個鎖的lock操作前發生。這個很好理解,鎖只有被釋放了才會被再次獲取
- volatile變量規則:對一個被volatile修飾的寫操作先發生于后面對該變量的讀操作
- 程序次序規則:一個線程內,按照代碼順序執行
- 線程啟動規則:Thread對象的start()方法先發生于此線程的其它動作
- 線程終結原則:線程的終止檢測后發生于線程中其它的所有操作
- 線程中斷規則: 對線程interrupt()方法的調用先發生于對該中斷異常的獲取
- 對象終結規則:一個對象構造先于它的finalize發生
- 編譯器和處理器會對指令進行重排序以提高性能,重排序有三種類型:
- 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
- 指令級別的重排序。現代處理器采用了指令級并行技術來將多條指令重疊執行。如果不存在數據依賴,處理器可以改變語句對應機器指令的執行順序。
- 內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是亂序執行。
-
這些重排序都可能會導致多線程程序出現內存可見性問題。
對于處理器重排序,JMM會要求編譯器在生成指令序列時插入特定類型的 內存屏障 指令來禁止特定類型的處理器重排。
JMM屬于語言級別的內存模型,它確保在不同的編譯器和不同的處理器平臺上,通過禁止一些重排序問題來保證內存可見性。
-
as-if-serial語義
是指不管怎么重排序,單線程程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
所以,編譯器和處理器不會對存在數據依賴關系的操作做重排序,因為這種重排序會改變執行結果。
-
數據依賴性
- 有三種類型:
- 寫后讀:a = 1; b = a;
- 寫后寫:a = 1; a = 2;
- 讀后寫: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++; } }
</ul>
- 順序一致性內存模型(為程序員提供了極強的內存可見性保證)的兩大特性:
- 一個線程中的所有操作必須按照程序的順序來執行
- 所有線程都只能看到一個單一的操作執行順序。每個操作都必須原子執行且立刻對所有線程可見。
-
其中對順序一致性和原子性的區別
原子性保證操作的原子性,而不是順序的一致性。
- 一旦一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾之后,那么就具備了兩層語義:
- 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
volatile 變量保證的是一個線程對它的寫會立即刷新到主內存中,并置其它線程的副本為無效,它并不保證對 volatile 變量的操作都是具有原子性的。 - 禁止進行指令重排序。
- 保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
- 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內存模型》
來自:https://juejin.im/post/58de675ea22b9d0058606fb8
- 某些情況下是指:
3. 原子性
原子性指:一個操作(有可能包含有多個子操作)要么全部執行(生效),要么全部都不執行(都不生效)。
java.util.concurrent.atomic包中很多類使用了CAS指令來保證原子性,而不再使用鎖。如 AtomicInterger 、 AtomicBoolean 、 AtomicLong 、 AtomicReference 等。
原子性不保證順序一致性,只保證操作是原子的。
4. 內存可見性
可見性是指,當多個線程并發訪問共享變量時,一個線程對共享變量的修改,其它線程能夠立即看到。
5. happens-before
happens-before規則對應于一個或多個編譯器和處理器重排序規則,對于程序員來說,該規則易懂,避免為了理解JMM提供的內存可見性保證而去學習復雜的重排序規則以及這些規則的具體實現。
使用happens-before的概念來 闡述操作之間的內存可見性 。
如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系。
這兩個操作可以在一個線程內,也可以是不同線程。
兩個操作之間具有happens-before關系,并不意味著前一個操作必須要在后一個操作前執行;僅僅要求前一個操作的執行結果對后一個可見,且前一個操作按順序排在第二個操作之前。
6. CAS指令
是現代處理器上提供的高效機器級別的原子指令,這些原子指令以原子方式對內存執行讀-寫-改操作,這是在多處理器中實現同步的關鍵。
AtomicInterger 、 AtomicBoolean 、 AtomicLong 的實現都是基于CAS指令。
7. 重排序
在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,盡可能的提高開發并行度。
8. 順序一致性
如果一個多線程程序能正確同步,這個程序將是一個沒有數據競爭的程序。JMM對正確同步的多線程程序的內存一致性做了如下保證:如果程序是正確同步的,程序的執行將具有順序一致性,即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。
9. volatile域
首先要明確,線程的安全性需要三點保證:原子性、可見性,順序性。只有滿足了這三個條件時線程才是安全的。