1. 程式人生 > 實用技巧 >【013期】JavaSE面試題(十三):多執行緒(3)

【013期】JavaSE面試題(十三):多執行緒(3)

開篇介紹

大家好,我是Java最全面試題庫提褲姐,今天這篇是JavaSE系列的第十三篇,主要總結了Java中的多執行緒問題,多執行緒分為三篇來講,這篇是第三篇,在後續,會沿著第一篇開篇的知識線路一直總結下去,做到日更!如果我能做到百日百更,希望你也可以跟著百日百刷,一百天養成一個好習慣。

volatile關鍵字的作用?

對於可見性,Java提供了volatile關鍵字來保證可見性。當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。主要的原理是使用了記憶體指令。

  • LoadLoad重排序:一個處理器先執行一個L1讀操作,再執行一個L2讀操作;但是另外一個處理器看到的是先L2再L1
  • StoreStore重排序:一個處理器先執行一個W1寫操作,再執行一個W2寫操作;但是另外一個處理器看到的是先W2再W1
  • LoadStore重排序:一個處理器先執行一個L1讀操作,再執行一個W2寫操作;但是另外一個處理器看到的是先W2再L1
  • StoreLoad重排序:一個處理器先執行一個W1寫操作,再執行一個L2讀操作;但是另外一個處理器看到的是先L2再W1

說一下volatile關鍵字對原子性、可見性以及有序性的保證

在volatile變數寫操作的前面會加入一個Release屏障,然後在之後會加入一個Store屏障,這樣就可以保證volatile寫跟Release屏障之 前的任何讀寫操作都不會指令重排,然後Store屏障保證了,寫完資料之後,立馬會執行flush處理器快取的操作 。
在volatile變數讀操作的前面會加入一個Load

屏障,這樣就可以保證對這個變數的讀取時,如果被別的處理器修改過了,必須得從其他 處理器的快取記憶體(或者主記憶體)中載入到自己本地快取記憶體裡,保證讀到的是最新資料; 在之後會加入一個Acquire屏障,禁止volatile讀操作之後的任何讀寫操作會跟volatile讀指令重排序。
與volatie讀寫記憶體屏障對比一下,是類似的意思。
Acquire屏障其實就是LoadLoad屏障 + LoadStore屏障
Release屏障其實就是StoreLoad屏障 + StoreStore屏障

什麼是CAS?

CAS(compare and swap)的縮寫。Java利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞演算法,實現原子操作。其它原子操作都是利用類似的特性完成的。
CAS有3個運算元:記憶體值V

舊的預期值A要修改的新值B
當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。
CAS的缺點:

  • CPU開銷過大
    在併發量比較高的情況下,如果許多執行緒反覆嘗試更新某一個變數,卻又一直更新不成功,迴圈往復,會給CPU帶來很到的壓力。
  • 不能保證程式碼塊的原子性
    CAS機制所保證的知識一個變數的原子性操作,而不能保證整個程式碼塊的原子性。比如需要保證3個變數共同進行原子性的更新,就不得不使用synchronized了。
  • ABA問題
    這是CAS機制最大的問題所在。

什麼是AQS?

AQS,即AbstractQueuedSynchronizer,佇列同步器,它是Java併發用來構建鎖和其他同步元件的基礎框架。
同步元件對AQS的使用:
AQS是一個抽象類,主是是以繼承的方式使用。
AQS本身是沒有實現任何同步介面的,它僅僅只是定義了同步狀態的獲取和釋放的方法來供自定義的同步元件的使用。從圖中可以看出,在java的同步元件中,AQS的子類(Sync等)一般是同步元件的靜態內部類,即通過組合的方式使用。
抽象的佇列式的同步器,AQS定義了一套多執行緒訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch
它維護了一個volatile int state(代表共享資源)和一個FIFO(雙向佇列)執行緒等待佇列(多執行緒爭用資源被阻塞時會進入此佇列)

Semaphore是什麼?

Semaphore就是一個訊號量,它的作用是限制某段程式碼塊的併發數。
semaphore有一個建構函式,可以傳入一個int型整數n,表示某段程式碼最多隻有n個執行緒可以訪問,如果超出了n,那麼請等待,等到某個執行緒執行完畢這段程式碼塊,下一個執行緒再進入。
由此可以看出如果Semaphore建構函式中傳入的int型整數n=1,相當於變成了一個synchronized了。

public static void main(String[] args) {  
        int N = 8; //工人數  
        Semaphore semaphore = new Semaphore(5); //機器數目  
        for(int i=0;i<N;i++)  
            new Worker(i,semaphore).start();  
    }      
    static class Worker extends Thread{  
        private int num;  
        private Semaphore semaphore;  
        public Worker(int num,Semaphore semaphore){  
            this.num = num;  
            this.semaphore = semaphore;  
        }          
        @Override  
        public void run() {  
            try {  
                semaphore.acquire();  
                System.out.println("工人"+this.num+"佔用一個機器在生產...");  
                Thread.sleep(2000);  
                System.out.println("工人"+this.num+"釋放出機器");  
                semaphore.release();              
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  

Synchronized的原理是什麼?

Synchronized是由JVM實現的一種實現互斥同步的方式,檢視被Synchronized修飾過的程式塊編譯後的位元組碼,會發現,被Synchronized修飾過的程式塊,在編譯前後被編譯器生成了monitorenter和monitorexit兩個位元組碼指令。
在虛擬機器執行到monitorenter指令時,首先要嘗試獲取物件的鎖:如果這個物件沒有鎖定,或者當前執行緒已經擁有了這個物件的鎖,把鎖的計數器+1;
當執行monitorexit指令時,將鎖計數器-1;當計數器為0時,鎖就被釋放了。如果獲取物件失敗了,那當前執行緒就要阻塞等待,直到物件鎖被另外一個執行緒釋放為止。
Java中Synchronize通過在物件頭設定標誌,達到了獲取鎖和釋放鎖的目的。

為什麼說Synchronized是非公平鎖?

非公平主要表現在獲取鎖的行為上,並非是按照申請鎖的時間前後給等待執行緒分配鎖的,每當鎖被釋放後,任何一個執行緒都有機會競爭到鎖,這樣做的目的是為了提高執行效能,缺點是可能會產生執行緒飢餓現象。

JVM對java的原生鎖做了哪些優化?

在Java6之前, Monitor的實現完全依賴底層作業系統的互斥鎖來實現.

由於Java層面的執行緒與作業系統的原生執行緒有對映關係,如果要將一個執行緒進行阻塞或喚起都需要作業系統的協助,這就需要從使用者態切換到核心態來執行,這種切換代價十分昂貴,很耗處理器時間,現代JDK中做了大量的優化。
一種優化是使用自旋鎖,即在把執行緒進行阻塞操作之前先讓執行緒自旋等待一段時間,可能在等待期間其他執行緒已經解鎖,這時就無需再讓執行緒執行阻塞操作,避免了使用者態到核心態的切換。
現代JDK中還提供了三種不同的 Monitor實現,也就是三種不同的鎖:

  • 偏向鎖(Biased Locking)
  • 輕量級鎖
  • 重量級鎖
    這三種鎖使得JDK得以優化 Synchronized的執行,當JVM檢測到不同的競爭狀況時,會自動切換到適合的鎖實現,這就是鎖的升級、降級。當沒有競爭出現時,預設會使用偏向鎖。
    JVM會利用CAS操作,在物件頭上的 Mark Word部分設定執行緒ID,以表示這個物件偏向於當前執行緒,所以並不涉及真正的互斥鎖,因為在很多應用場景中,大部分物件生命週期中最多會被一個執行緒鎖定,使用偏向鎖可以降低無競爭開銷。
    如果有另一執行緒試圖鎖定某個被偏向過的物件,JVM就撤銷偏向鎖,切換到輕量級鎖實現。
    輕量級鎖依賴CAS操作 Mark Word來試圖獲取鎖,如果重試成功,就使用普通的輕量級鎖否則,進一步升級為重量級鎖。

Synchronized和 ReentrantLock的異同?

synchronized
是java內建的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由JVM實現,使用者不需要顯示的釋放鎖,非常方便。然而synchronized也有一些問題:
當執行緒嘗試獲取鎖的時候,如果獲取不到鎖會一直阻塞。
如果獲取鎖的執行緒進入休眠或者阻塞,除非當前執行緒異常,否則其他執行緒嘗試獲取鎖必須一直等待。

ReentrantLock
ReentrantLock是Lock的實現類,是一個互斥的同步鎖。ReentrantLock是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成。
等待可中斷避免,出現死鎖的情況(如果別的執行緒正持有鎖,會等待引數給定的時間,在等待的過程中,如果獲取了鎖定,就返回true,如果等待超時,返回false)
公平鎖與非公平鎖多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,ReentrantLock預設的建構函式是建立的非公平鎖,可以通過引數true設為公平鎖,但公平鎖表現的效能不是很好。

從功能角度
ReentrantLock比 Synchronized的同步操作更精細(因為可以像普通物件一樣使用),甚至實現 Synchronized沒有的高階功能,如:

  • 等待可中斷當持有鎖的執行緒長期不釋放鎖的時候,正在等待的執行緒可以選擇放棄等待,對處理執行時間非常長的同步塊很有用。
  • 帶超時的獲取鎖嘗試在指定的時間範圍內獲取鎖,如果時間到了仍然無法獲取則返回。
  • 可以判斷是否有執行緒在排隊等待獲取鎖。
  • 可以響應中斷請求與Synchronized不同,當獲取到鎖的執行緒被中斷時,能夠響應中斷,中斷異常將會被丟擲,同時鎖會被釋放。
  • 可以實現公平鎖。

從鎖釋放角度
Synchronized在JVM層面上實現的,不但可以通過一些監控工具監控 Synchronized的鎖定,而且在程式碼執行出現異常時,JVM會自動釋放鎖定,但是使用Lock則不行,Lock是通過程式碼實現的,要保證鎖定一定會被釋放,就必須將 unLock()放到 finally{}中。

從效能角度
Synchronized早期實現比較低效,對比 ReentrantLock,大多數場景效能都相差較大。
但是在Java6中對其進行了非常多的改進,
在競爭不激烈時:Synchronized的效能要優於 ReetrantLock;
在高競爭情況下:Synchronized的效能會下降幾十倍,但是 ReetrantLock的效能能維持常態。