標籤:對象 接下來 local 互斥 意義 程式 藝術 ros 應用
Java並發編程(一)
之前看《Thinking In Java》時,並發講解的挺多的,自己算是初步瞭解了並發。但是其講解的不深入,自己感覺其講解的不夠好。後來自己想再學一學並發,買了《Java並發編程實戰》,看了一下講的好基礎、好多的理論,而且自我感覺講的邏輯性不強。最後,買了本《Java並發編程的藝術》看,這本書挺好的,邏輯性非常強。
1. 概述
本篇文章主要內容來自《Java並發編程的藝術》,其講解的比較深入,自己也有許多不懂的地方,然後自己主要把它講解的提煉出來。本章是Java並發編程的基礎知識,瞭解後能夠對Java並發有一些基本的瞭解,其中許多深入電腦系統層面的知識我都濾過。想要深入而具體的瞭解,請見《Java並發編程的藝術》。
2. 基本知識點
記憶體可見度:當一個線程修改一個共用變數(存放在堆中:如執行個體域、靜態域、數組等)時,另一個線程總能讀到修改後的值。
原子性(原子操作):不可以被中斷的一個或一系列操作。
重排序:在執行程式時,為了提高效能,編譯器和處理器常常對指令做重排序。
3. Java記憶體模型(JMM)
3.1 Java的並發編程採用共用記憶體模型
通訊(指線程之間以何種機制交換資訊):線程之間共用程式的公用狀態,通過寫-讀記憶體中的公用狀態進行隱式通訊。
同步(指程式中用於控制不同線程間操作發生相對順序的機制):程式員必須顯示的指定某個方法或某段代碼需要線上程之間互斥執行。
3.2 JMM定義了線程與主記憶體之間的抽象關係
線程之間的共用變數儲存在主記憶體(Main Memory)中,每個線程都有一個私人的本地記憶體(Local Memory),本地記憶體儲存了該線程以讀/寫共用變數的副本。所以,JMM通過控制主記憶體與每個線程的本地內容之間的互動,來為Java程式提供記憶體可見度。
(註:本地記憶體是一個抽象概念,並不真實存在,它包含緩衝、寫緩衝區、寄存器以及其他的硬體和編譯器最佳化。)
3.3 JMM不保證對64位的long類型與double類型的寫操作具有原子性,但讀操作具有原子性
原因:JMM對64位的寫操作會拆分為兩個32位的寫操作來執行。
4. happens-before
4.1 概念:Java中闡述操作之間的記憶體可見度
在JMM中,如果一個操作的結果需要對另一個操作可見,那麼這兩個操作之間存在happens-before關係。註:happens-before僅僅要對前一個操作(執行的結果)對後一操作可見,並不意味著操作順序。
4.2 happens-before的規則:
- 程式順序規則:一個線程中的每個操作,happens-before於該線程中的任意後續操作。註:JMM中只要求對該線程的結果不會變化,所以只要最後結果沒有變化,JMM容許作業系統的重排序。
- 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
- volatile變數規則:對一個volatile域的寫入,happens-before於任意後續對這個volatile域的讀。
- happens-before具有傳遞性。
5. volatile
5.1 volatile特性
- 記憶體可見度:對於一個volatile變數的讀,總能看到(任何線程)對這個volatile變數最後的寫入。
- 原子性:對於任意單個volatile變數的讀/寫具有原子性(例如volatile=*),但類似於volatile++這種複合操作不具有原子性。
5.2 volatile寫-讀建立的happens-before關係
volatile變數的寫-讀可以實現線程之間的通訊(隱式)。
如下代碼:
class VolatileExample { private int a = 0; private volatile boolean flag = false; void write() { this.a = 1;// 1 this.flag = true;// 2 } void read() { while (flag) {// 3 int i = a;// 4 ... } }}
如果線程A執行write()方法後,線程B執行read()方法。根據happens-before規則:B線程總能夠進入迴圈,並且獲得的a的值為1。
程式順序規則:1 happens-before 2;3 happens-before 4
volatile變數規則:2 happens-before 3
路由規則:1 happens-before 4
5.2 volatile寫-讀的記憶體意義
- 寫一個volatile變數時,JMM會把該線程對應的本地記憶體中的共用變數重新整理到主記憶體中,並通知接下來要讀取這個volatile變數的某個線程發出了(其對共用變數所做修改的)訊息。
- 讀一個volatile變數,實質上該線程接受到之前某個線程發出的修改訊息,JMM會把該線程對應的本地記憶體置為無效,線程接下來會從主記憶體中讀取共用變數。
6. 鎖
6.1 鎖與volatile的記憶體意義基本相似(隱式通訊)
- 鎖的釋放與volatile變數的寫有相同的記憶體意義。
- 鎖的擷取與volatile變數的讀有相同的記憶體意義。
6.2 鎖能夠讓臨界區互斥執行(顯式同步)
6.3 鎖與volatile變數的比較
- volatile僅僅能夠保證單個volatile變數的讀/寫具有原子性,而鎖能夠保證整個臨界區具有原子性。
- volatile效能比鎖好,而功能不如鎖。
註:ReentrantLock鎖的實現使用到了volatile變數。自己不是太懂實現機制,就不寫了,具體可以參見《Java並發編程的藝術》。
7. final域
7.1 記憶體意義:
只要final對象時正確構造的,那麼不需要使用同步(鎖與volatile)就可以保證任何線程都能夠看都該對象的final域在建構函式中被初始化的值。
8.應用
8.1 單例模式中可以使用雙重檢查鎖機制來建立單例
代碼如下:
class Singleton { private volatile static Singleton instance;// 1 private Singleton() {} public static Singleton getInstance() { if (instance == null) {// 2 synchronized (Singleton.class) { if (instance == null) { instance = new Singleton();// 3 } } } return instance; }}
注意:執行個體引用必須是volatile變數。如果不是,在運行到2時讀取到instance不為null,但是可能該instance所指向的對象還沒有完成初始化。原因:建立對象時的重排序,自己不太清楚,具體可以參見《Java並發編程的藝術》。
8.2 單例模式中基於類初始化的擷取單例(內部類)
代碼如下:
class Singleton { private Singleton() {} public static Singleton getInstance() { return SingletonHolder.instance; // 這裡才會導致SingletonHolder類的初始化 } private static class SingletonHolder{ static Singleton instance = new Singleton(); }}
優點:只有在第一次調用getInstance()方法時才會正式載入SingletonHolder類,也就是才會建立對象。
安全執行緒的原因:在類的初始化時期,JVM會擷取一個鎖,該鎖可以同步多個線程對同一個類的初始化。
Java並發編程(一)