前幾天再寫synchronized的JVM實現時候,想起若是是2個線程同時想得到對象鎖,即修改對象頭的lock位時候,同時發現對象鎖可用,同時修改lock的值,天了嚕想一想真是太可怕,想解決互斥卻在解決方法中也有互斥的問題。後來發現:java
任何互斥嘗試必須基於一些基礎硬件的互斥!算法
任何互斥嘗試必須基於一些基礎硬件的互斥!數組
任何互斥嘗試必須基於一些基礎硬件的互斥!併發
重要的話要說3遍!!jvm
那咱們來看看硬件的支持吧。spa
一、中斷禁用操作系統
在單處理器中,併發進程不能同時執行,只能交替。所以要保證互斥,只須要保證一個進程不被中斷就行,這種能力能夠經過系統內核啓用中斷和禁止中判定義的原語實現。線程
禁用中斷 臨界區 啓用中斷
這種方法代價很高,由於處理器只能交替執行程序。還有個問題對於多處理器並無什麼用。設計
二、專用機器指令code
在多處理器中,全部處理器都是共享內存。因此要保證互斥,在硬件級別上,對存儲單位的訪問要排斥對相同單位的其餘訪問。基於這一點,處理器設計者設計了一些機器指令,來保證兩個動做的完整性,如咱們喜聞樂見的compareAndSwap。
機器指令方法使用了忙等待。可能飢餓,致使某些進程無限期進入臨界區。最重要的是可能產生死鎖,假設進程P1經過CAS進入臨界區,可是在臨界區的時候被迫中斷把處理器讓給優先級別更高的P2,若是P2想使用同一資源,因爲互斥機制,那麼P2將永遠沒法進入臨界區。
對存儲單位的訪問要排斥對相同單位的其餘訪問又是怎麼實現的呢?舉個列子,在x86 平臺上,CPU提供了在指令執行期間對總線加鎖的手段。CPU芯片上有一條引線#HLOCK pin,若是彙編語言的程序中在一條指令前面加上前綴"LOCK",通過彙編之後的機器代碼就使CPU在執行這條指令的時候把#HLOCK pin的電位拉低,持續到這條指令結束時放開,從而把總線鎖住,這樣同一總線上別的CPU就暫時不能經過總線訪問內存了,保證了這條指令在多處理器環境中的原子性。
軟件方法(見下文)和硬件方法都有明顯缺陷,咱們須要尋找其餘機制。
信號量
其基本原理是:兩個或者多個進程能夠經過簡單的信號進行合做,一個進程能夠被迫停在某個位置,直到它收到特定的信號。爲了達到預期效果,咱們能夠把信號量當作一個整型變量,在它之上定義3個操做
1)一個信號量初始化非負整數
2)semWaite操做使信號量減一。若是信號量變爲負數,則執行semWaite的進程被阻塞。
3)semSignal操做使信號量加一。若是信號量不爲負數,則被semWaite阻塞的一個進程解除阻塞
固然semWaite和semSignal都是原語,並且都具備原子性。至於還有二元信號量,管程之類的機制,你們有興趣能夠看看操做系統,下面咱們要講講軟件實現的互斥。
軟件實現的互斥固然也要按照基本法,對存儲單位的訪問要排斥對相同單位的其餘訪問這個假設確定是要有的。那一拍腦殼,確定說設置個變量,每一個進程檢查變量,若是爲true則把值設爲false,訪問臨界區,訪問完之後把值設爲true,以下:
volatile boolean flag=true; while(flag) flag=false; //訪問臨界區 flag=true;
再仔細一想,這基本的互斥都作不到,假設P1線程先訪問flag,此時flag爲true,線程P1還沒修改flag值,線程P2訪問flag值,而後P1,P2就一塊兒愉快的訪問臨界區了,但是咱們不樂意啊,因此咱們要修改程序,分開P1,P2這對小婊砸。
P1: while(flag) //訪問臨界區 flag=true; P2: while(!flag) //訪問臨界區 flag=false;
這樣是分開了P1,P2,可是並非很優雅,P1,P2老是交替執行,更可怕的是若是P1在訪問臨界區的時候不當心退出,那麼P2將永遠不能訪問臨界區。接下來我就不賣蠢了,直接貼出Peterson算法的java實現。
public class Petersen { private volatile static boolean flags[]; private volatile static int signal; public static void main(String args[]) { flags = new boolean[2]; new Thread(new Runnable() { public void run() { while (true) { flags[0] = true; signal = 0; while (flags[1] && signal == 0) { } //臨界區,do something for (int i = 0; i < 10; i++) { try { Thread.sleep(100l); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getId()); } //退出臨界區 flags[0]=false; break; } } }).start(); new Thread(new Runnable() { public void run() { while (true) { flags[1] = true; signal = 1; while (flags[0] && signal == 1) { } //臨界區,do something for (int i = 0; i < 10; i++) { try { Thread.sleep(100l); } catch (InterruptedException e) { } System.out.println(Thread.currentThread().getId()); } //退出臨界區 flags[1]=false; break; } } }).start(); } }
原理是利用一個boolean數組記錄各線程的狀態,而後用signal來控制來控制同步併發。假設兩進程都想進入臨界區,把本身flag設置爲true,把signal設置爲本身線程ID,咱們能夠看見總有一個線程在while那邊忙循環,從而保持了互斥。據說peterson算法很容易推廣到N線程,可是試了試3線程,老是不能很好控制,有好心人知道麻煩告知一聲。
在看java互斥的實現下咱們先來看看java的內存模型(對理解volatile有極大用處):
Java內存模型規定,對於多個線程共享的變量,存儲在主內存當中,每一個線程都有本身獨立的工做內存,線程只能訪問本身的工做內存,不能夠訪問其它線程的工做內存。工做內存中保存了主內存共享變量的副本,線程要操做這些共享變量,只能經過操做工做內存中的副原本實現,操做完畢以後再同步回到主內存當中。如何保證多個線程操做主內存的數據完整性是一個難題,Java內存模型也規定了工做內存與主內存之間交互的協議,首先是定義了8種原子操做:
(1) lock:將主內存中的變量鎖定,爲一個線程所獨佔
(2) unclock:將lock加的鎖定解除,此時其它的線程能夠有機會訪問此變量
(3) read:將主內存中的變量值讀到工做內存當中
(4) load:將read讀取的值保存到工做內存中的變量副本中。
(5) use:將值傳遞給線程的代碼執行引擎
(6) assign:將執行引擎處理返回的值從新賦值給變量副本
(7) store:將變量副本的值存儲到主內存中。
(8) write:將store存儲的值寫入到主內存的共享變量當中。
那synchronized是怎樣獲取對象鎖的呢,固然它可使用lock原子性操做,也能夠用peterse同樣的算法,具體實現得看看jvm的源碼。以上——