天天看點

Java并發程式設計系列之一:并發機制的底層原理

前言

并發程式設計的目的是讓程式運作更快,但是使用并發并不定會使得程式運作更快,隻有當程式的并發數量達到一定的量級的時候才能展現并發程式設計的優勢。是以談并發程式設計在高并發量的時候才有意義。雖然目前還沒有開發過高并發量的程式,但是學習并發是為了更好了解一些分布式架構。那麼當程式的并發量不高,比如是單線程的程式,單線程的執行效率反而比多線程更高。這又是為什麼呢?熟悉作業系統的應該知道,cpu是通過給每個線程配置設定時間片的方式實作多線程的。這樣,當cpu從一個任務切換到另一個任務的時候,會儲存上一個任務的狀态,當執行完這個任務的時候cpu就會繼續上一個任務的狀态繼續執行。這個過程稱為上下文切換。

在java多線程中,volatile關鍵字個synchronized關鍵字扮演了重要的角色,它們都可以實作線程的同步,但是在底層是如何實作的呢?

volatile關鍵字

我們知道,java虛拟機把java源檔案編譯成.class檔案,然後把class檔案加載到記憶體中,jvm執行位元組碼,最終轉化為彙編指令在cpu上執行。

volatile定義:

java程式設計語言允許線程通路共享變量,為了確定共享變量能夠準确一緻地更新,線程確定通過排他鎖單獨獲得這個變量。

java中的volatile關鍵字就是這個定義的展現。如果一個變量被聲明為volatile,那麼確定這個變量是“可見的”。可見性的意思是當線程修改一個共享變量的時候,另外一個線程能夠讀到這個修改的值。由于底層的實作涉及處理器(這裡以x86處理器作為舉例),是以有必要先了解一些cpu術語:

術語

描述

記憶體屏障

是一組處理器指令,用于實作記憶體操作的順序限制

緩存行

緩存中可以配置設定的最小存儲機關

原子操作

不可中斷的一系列操作

緩存行填充

當處理器識别到從記憶體中讀取的操作數是可緩存的,處理器讀取整個緩存行到适當的緩存

下面,volatile是如何保證可見性的呢?當一個變量被聲明為volatile的時候,底層(更具體是處理器)會幫我們完成下面的事情,有兩句話很重要:

将目前處理器緩存行的資料寫回到系統記憶體中

這個寫回到記憶體的資料會使得其他cpu(比如是多核處理器)裡緩存了該記憶體位址的資料無效

為了提高處理速度,保證記憶體可見性的實作,cpu之間會使用緩存一緻性協定,具體講就是:如果對聲明為volatile關鍵字的變量進行寫操作,那麼jvm就會向處理器發送一條lock字首的指令,進而将緩存行中的資料寫回到系統記憶體中。是不是這樣就夠了呢?因為即使寫回到系統記憶體中,而其他處理器仍然其緩存中的資料怎麼辦,是以每個處理器會通過嗅探技術在總線上傳播資料來檢查自己緩存中資料是不是最新的,如果發現不是最新的就把緩存行設定為無效,處理器下次就會直接從系統記憶體中取資料,因為系統記憶體中資料是最新的。

如何讓緩存行中資料寫回到系統記憶體中?之前的處理器是通過鎖總線的方式獨占共享記憶體,這樣做的缺點很明顯:導緻其他處理器不能通路總線,這樣就不能通路記憶體。現在的處理器已經是使用緩存鎖定的方式來保證原子性,具體就是:如果執行的寫操作的記憶體區域已經緩存在處理器的内部,就會鎖定這塊記憶體區域的緩存并寫回到記憶體中,并使用緩存一緻性協定確定修改的原子性。

怎樣使得其他處理器緩存行中的資料無效?簡要概括就是mesi(修改、獨占、共享、無效)。這是一個控制協定,可以維護内部緩存和其他處理器緩存的一緻性。一個處理器通過嗅探技術得知其他處理器打算寫記憶體位址,而這個位址目前處于共享狀态(意味着其他處理可以通路),那麼正在嗅探的處理器會把其緩存行設定為無效。

synchronized關鍵字

使用synchronized關鍵字有以下三種使用方式:

同步代碼塊

同步方法

靜态同步方法

對于同步代碼塊鎖的是括号裡面的配置對象,同步方法鎖的是目前執行個體對象,靜态同步方法鎖的是目前類的class對象。jvm通過一個螢幕對象實作代碼塊的同步,對應的指令是monitorenter和monitorexit,方法的同步使用的另一種機制,但仍然使用的是螢幕對象來實作同步的。具體是這樣的:monitorenter指令插入到同步代碼塊中的開始位置,而monitorexit指令則被插入到方法結束處和異常處。要注意的是,任何對象都有一個螢幕對象與之關聯,當一個螢幕對象被持有的時候,該對象将處于鎖定狀态。當線程執行到monitorenter指令的時候,将會嘗試擷取對象所對應的螢幕對象的所有權,也就是線程的鎖。

鎖的使用

前面提到了所得概念,那麼鎖有哪些呢?在jdk 1.6中引入了“偏向鎖”和“輕量級鎖“。鎖一共有四種狀态:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖隻能更新,不能降級。當對鎖的競争加劇的時候,鎖會發生更新。

1.偏向鎖

之是以引入偏向鎖,是為了讓線程獲得鎖的代價更低。當一個線程通路同步塊并擷取鎖的時候,會在對象的對象頭(對象頭包括兩部分的資訊:一部分是”mark word“,主要存放的是哈希碼、對象的分代年齡、鎖的标記等資訊;另一部分是對象的類型指針)和棧幀中的鎖記錄中存儲鎖偏向的id,以後該線程在進入方法的同步塊的時候,就檢查這個id(可以了解為一種标記,是一種身份的辨別),如果測試成功,表明對象已經獲得了鎖;如果測試失敗,繼續測試偏向鎖的辨別是否設定為1(1的話就是偏向鎖),如果沒有則使用cas(compare and swap)鎖。

2.輕量級鎖

分為加鎖和解鎖。當線程執行到同步塊之前,jvm會首先檢查目前線程的棧幀中建立用于存儲記錄鎖記錄的空間,并将對象頭中mark word複制到鎖記錄中,也稱為displaced mark word,然後線程嘗試使用cas将對象頭中的mark word替換為指向鎖記錄的指針。如果成功,則線程獲得鎖,否則目前線程嘗試使用自旋來擷取鎖。這就是加鎖的過程。

這裡多次提到cas,那麼cas是個什麼鬼?cas是compare and swap(比較和替換)的簡寫,具體而言就是:當進行cas操作的時候,需要輸入兩個數值,一個是舊值,該舊值是原來的值,另一個是新值,也就是發生改變的值,得到這兩個值後,在cas操作期間會去比較舊值是否發生變化,如果沒有發生變化就用新值進行替換,如果發生了變化就不進行替換。

那麼解鎖的過程又是怎樣的呢?就是使用cas操作将displaced mark word替換回對象頭,如果成功,則表示沒有競争發生。如果失敗,表示目前鎖存在競争,鎖就會膨脹,膨脹的結果是導緻鎖的更新,并進入阻塞狀态。直到需要釋放鎖的線程釋放鎖并喚醒其他等待的線程。

鎖的使用場景

由于偏向鎖線上程存在競争的時候會帶來額外的性能開銷,是以偏向鎖适用于隻有一個線程方法同步快的情況;輕量級鎖線上程競争鎖的情況下不會導緻線程阻塞,但是會通過自旋消耗cpu,是以輕量級鎖适用于追求響應時間的情況。重量級鎖線程競争不會使用自旋,但是線程競争會導緻阻塞,是以響應時間比較慢,重量級鎖一般使用在追求吞吐量的情況。