Java面試準備-鎖機制
問題連結轉載 Java面試通關要點彙總集【終極版】
一、說說執行緒安全問題
- 執行緒安全問題是多個執行緒類併發操作某類的函式,修改某個成員變數的值,很容易造成錯誤,資料不同步。
- 執行緒安全出現問題的根本原因是存在多個執行緒物件共享同個資源或操作共享資原始碼有多個語句
- 常用的解決方案有同步程式碼塊或同步函式
- 同步程式碼塊
格式:synchronized (鎖物件) {
// 需要被同步的程式碼
}
注意事項:
- 鎖物件可以是任意的一個物件
- 一個執行緒在同步程式碼中sleep也不會釋放鎖物件
- 如果不存線上程安全問題,千萬別使用同步程式碼塊,相對降低了效率,因為同步外的執行緒的都會判斷同步鎖
- 鎖物件必須是多執行緒共享的一個資源,否則鎖不住
- 同步函式,使用synchronized修飾一個函式
注意事項:
- 如果函式是一個非靜態的同步函式,鎖物件是this物件
- 如果函式是靜態的同步函式,鎖物件就是當前函式所屬的類的位元組碼檔案(class物件)
- 同步函式的鎖物件是固定的,不能由自己指定
//同步程式碼塊 synchronized(obj){ // ... } //同步函式 public static synchronized void show(){ // .... }
死鎖
public class Beetle implements Runnable{ private boolean flag; Beetle(boolean flag){ this.flag = flag; } public void run(){ if(flag){ while(true){ synchronized(MyLock.locka){ System.out.println(Thread.currentThread().getName()+"..if locka..."); synchronized(MyLock.lockb){ System.out.println(Thread.currentThread().getName()+"..if lockb..."); } } } }else{ while(true){ synchronized(MyLock.lockb){ System.out.println(Thread.currentThread().getName()+"..if lockb..."); synchronized(MyLock.locka){ System.out.println(Thread.currentThread().getName()+"..if locka..."); } } } } } public static void main(String[] args){ Beetle a = new Beetle(true); Beetle b = new Beetle(false); Thread t1 = new Thread(a); Thread t2 = new Thread(b); t1.start(); t2.start(); } } class MyLock{ public static final Object locka = new Object(); public static final Object lockb = new Object(); }
餓漢式,沒有執行緒安全問題
//餓漢式
class Single {
private static final Single s = new Single();
private Single(){}
public static Single getInstance(){
return s;
}
}
//懶漢
class Single{
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(s == null){
synchronized(Single.class){
if(s == null)
s = new Single();
}
}
return s;
}
}
ArrayList是非執行緒安全的,Vector是執行緒安全的;HashMap是非執行緒安全的,HashTable是執行緒安全的;StringBuilder是非執行緒安全的,StringBuffer是執行緒安全的
二、volatile 實現原理
有volatile變數修飾的共享變數進行寫操作的時候會多出第二行彙編程式碼,裡頭有個Lock字首指令。而這個指令在多核處理器下引發兩件事:
- 將當前處理器快取行的資料寫回到系統記憶體
- 這個寫回記憶體的操作會使在其他CPU裡快取了該記憶體地址的資料無效
如果對聲明瞭volatile的變數進行寫操作,JVM就會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫回到記憶體。每個處理器通過嗅探在總線上傳播的資料來檢查自己快取的值是否過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定為無效狀態,當處理器對這個資料進行修改操作時會重新從系統記憶體中把資料讀到處理器快取裡。
三、synchronize 實現原理
Java中每個物件都可以作為鎖,這是synchronized實現同步的基礎:
- 普通同步方法,鎖是當前例項物件
- 靜態同步方法,鎖是當前類的class物件
- 同步方法塊,鎖是括號裡的物件
同步程式碼塊:monitorenter指令插入到同步程式碼塊的開始位置,monitorexit指令插入到同步程式碼塊的結束位置。當且一個monitor被持有之後,它將於鎖定狀態。執行緒執行到monitorenter指令時將會嘗試獲取物件所對應的monitor所有權,即嘗試獲取物件的鎖。
同步方法:synchronized方法則會被翻譯成普通的方法呼叫和返回指令。在VM位元組碼層面並沒有任何特別的指令來實現被synchronized修飾的函式,而在Class檔案的方法表將該方法的access_flags欄位中的synchronized標誌位置1,表示該方法是同步方法並使用呼叫該方法的物件或該方法所屬的Class在JVM的內部物件表示Klass作為鎖物件
四、synchronized 與 lock 的區別
- synchronized是java的內建關鍵字,在JVM層面,Lock是個java類
- synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖
- synchronized會自動釋放鎖(執行緒執行完同步程式碼會釋放或發生異常會釋放),Lock需要在finally中手動釋放鎖(unlock()釋放鎖),否則容易造成執行緒死鎖
- synchronized關鍵字的兩個執行緒1和執行緒2,如果當前執行緒1獲得鎖,執行緒2等待。如果執行緒1阻塞,執行緒2會一直等待。而Lock鎖不一定會等待下去,如果嘗試獲取不到鎖,執行緒可以不用一直等待
- synchronized的鎖可重入,不可中斷,非公平。而Lock鎖可重入,可判斷,可公平
- Lock鎖適合大量同步的程式碼的同步問題,synchronized鎖適合程式碼少量的同步問題
五、CAS 樂觀鎖
1 Java中的Synchronized屬於悲觀鎖
2 樂觀鎖的核心演算法是CAS(Compareand Swap,比較並交換),它涉及到三個運算元:記憶體值,預期值和新值,當且僅當預期值和記憶體值相等時才將記憶體值修改為新值。其邏輯為首先檢查某塊記憶體的值是否跟之前我讀取時一樣,如不一樣則表示期間此記憶體值已被別的執行緒更改過,捨棄本次操作,否則說明期間沒有其他執行緒對此記憶體值操作,可以把新值設定給此塊記憶體。
public class Beetle implements Runnable{
private static AtomicBoolean flag = new AtomicBoolean(true);
public static void main(String[] args){
Beetle b = new Beetle();
Thread t1 = new Thread(b);
Thread t2 = new Thread(b);
t1.start();
t2.start();
}
public void run(){
System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
if(flag.compareAndSet(true, false)){
System.out.println(Thread.currentThread().getName()+""+flag.get());
try{
Thread.sleep(5000);
}catch(InterruptedException e){
e.printStackTrace();
}
flag.set(true);
}else{
System.out.println("重試機制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
try{
Thread.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
run();
}
}
}
CAS的缺點:
1.CPU開銷較大
在併發量比較高的情況下,如果許多執行緒反覆嘗試更新某一個變數,卻又一直更新不成功,迴圈往復,會給CPU帶來很大的壓力。
2.不能保證程式碼塊的原子性
CAS機制所保證的只是一個變數的原子性操作,而不能保證整個程式碼塊的原子性。比如需要保證3個變數共同進行原子性的更新,就不得不使用Synchronized了。
六、ABA 問題
在運用CAS做Lock-Free操作中有一個經典的ABA問題:
執行緒1準備用CAS將變數的值由A替換為B,在此之前,執行緒2將變數的值由A替換為C,又由C替換為A,然後執行緒1執行CAS時發現變數的值仍然為A,所以CAS成功。但實際上這時的現場已經和最初不同了,儘管CAS成功,但可能存在潛藏的問題,例如下面的例子:
現有一個用單向連結串列實現的堆疊,棧頂為A,這時執行緒T1已經知道A.next為B,然後希望用CAS將棧頂替換為B:
head.compareAndSet(A,B);
在T1執行上面這條指令之前,執行緒T2介入,將A、B出棧,再pushD、C、A,此時堆疊結構如下圖,而物件B此時處於遊離狀態:
此時輪到執行緒T1執行CAS操作,檢測發現棧頂仍為A,所以CAS成功,棧頂變為B,但實際上B.next為null,所以此時的情況變為:
其中堆疊中只有B一個元素,C和D組成的連結串列不再存在於堆疊中,平白無故就把C、D丟掉了。
七、樂觀鎖的業務場景及實現方式
樂觀鎖是在應用層加鎖,而悲觀鎖是在資料庫層加鎖(for update)
樂觀鎖顧名思義就是在操作時很樂觀,這資料只有我在用,我先儘管用,最後發現不行時就回滾。
樂觀鎖的核心演算法是CAS(Compareand Swap,比較並交換),它涉及到三個運算元:記憶體值,預期值和新值,當且僅當預期值和記憶體值相等時才將記憶體值修改為新值。其邏輯為首先檢查某塊記憶體的值是否跟之前我讀取時一樣,如不一樣則表示期間此記憶體值已被別的執行緒更改過,捨棄本次操作,否則說明期間沒有其他執行緒對此記憶體值操作,可以把新值設定給此塊記憶體。
額外知識:
在併發程式設計中我們一般都會遇到這三個基本概念:原子性,可見性和有序性
- 原子性:即一個操作或多個操作,要麼全部執行並且執行過程不會被任何因素打斷,要麼就都不執行
i = 0 // 是原子性操作
j = i // 不是原子性操作,包含了兩個操作:讀取 i,將其賦值給 j
i ++ // 不是原子性操作,包含了三個操作:讀取 i,i + 1 , 將結果賦值給 i
i = j+1 // 不是原子性操作,包含了三個操作:讀取 j,i + 1 , 將結果賦值給 i
- 可見性:當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值
- 有序性:即程式執行的順序按照程式碼的先後順序執行