關於java中的鎖(轉)


對於鎖一直處於比較模糊的狀態,最近一天晚上偶然想看看,就翻了幾本書,然后弄明白了一些概念,有一些仍然沒明白,例如AQS,先把搞明白的記錄一下吧。

 

什么是線程安全?

當多個線程訪問一個對象時,如果不用考慮這些線程在運行時環境的調度和交替執行,也不需要進行額外的同步,或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那這個對象就是線程安全的。代碼本省封裝了所有必要的正確性保障手段(互斥同步等),令調用者無需關心多線程的問題,更無需自己實現任何措施來保證多線程的正確調用。

 

線程之間的交互機制?

不同的線程之間會產生競爭,同樣也有交互,最典型的例如數據庫連接池,一組數據庫連接放在一個數組中,如果線程需要數據庫操作,則從池中獲取鏈接,用完了就放回去。JVM提供了wair/notify/notifyAll方式來滿足這類需求,典型的代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package  lock;
public  class  Pool {
     public  Connection get(){
         synchronized  ( this ) {
             if (free> 0 ){
                 free--;
             } else {
                 this .wait();
             }
             return  cacheConnection.poll();
 
         }
     }
     public  void  close(Connection conn){
         synchronized  ( this ) {
             free++;
             cacheConnection.offer(conn);
             this .notifyAll();
         }
     }
}

在JDK5之后,並發包中提供了更多的方式來進行線程之間的交互,例如Condition中的await和signal,CountDownLatch中的countDown;

 

如何實現線程安全?

A、互斥同步,最常見的並發正確性保障手段,同步至多個線程並發訪問共享數據時,保證共享數據在同一個時刻只被一個線程使用。

B、非阻塞同步,互斥同步的主要問題就是進行線程的阻塞和喚醒所帶來的性能問題,因此這個同步也被稱為阻塞同步,阻塞同步屬於一種悲觀的並發策略,認為只要不去做正確的同步措施,就肯定會出問題,無論共享的數據是否會出現競爭。隨着硬件指令的發展,有了另外一個選擇,基於沖突檢測的樂觀並發策略,通俗的講就是先進性操作,如果沒有其他線程爭用共享數據,那操作就成功了,如果共享數據有爭用,產生了沖突,那就再進行其他的補償措施(最常見的措施就是不斷的重試,直到成功為止),這種策略不需要把線程掛起,所以這種同步也被稱為非阻塞同步。

C、無同步方案,簡單的理解就是沒有共享變量需要不同的線程去爭用,目前有兩種方案,一個是“可重入代碼”,這種代碼可以在執行的任何時刻中斷它,轉而去執行其他的另外一段代碼,當控制權返回時,程序繼續執行,不會出現任何錯誤。一個是“線程本地存儲”,如果變量要被多線程訪問,可以使用volatile關鍵字來聲明它為“易變的“,以此來實現多線程之間的可見性。同時也可以通過ThreadLocal來實現線程本地存儲的功能,一個線程的Thread對象中都有一個ThreadLocalMap對象,來實現KV數據的存儲。

 

主內存和工作內存?

Java內存模型中規定了所有變量都存儲在主內存中,每個線程還有自己的工作內存,線程的工作內存中保存了改線程使用到的變量的主內存副本拷貝,線程對於變量的所有操作(讀取和賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量,不同線程之間無法直接訪問對方工作內存中的變量,線程間值的傳遞均需要通過主內存來完成。這里的主內存和工作內存,和java中堆的模型不是一個層次,主內存主要對應java堆中對象的實例數據部分。

 

什么是自旋鎖?

 自旋鎖在JDK1.6之后就默認開啟了。基於之前的觀察,共享數據的鎖定狀態只會持續很短的時間,為了這一小段時間而去掛起和恢復線程有點浪費,所以這里就做了一個處理,讓后面請求鎖的那個線程在稍等一會,但是不放棄處理器的執行時間,看看持有鎖的線程能否快速釋放。為了讓線程等待,所以需要讓線程執行一個忙循環也就是自旋操作。

在jdk6之后,引入了自適應的自旋鎖,也就是等待的時間不再固定了,而是由上一次在同一個鎖上的自旋時間及鎖的擁有者狀態來決定。

 

什么是鎖消除?

虛擬機即時編譯器在運行時,對於代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。如果判斷一段代碼,在椎上的所有數據都不會逃逸出去被其他線程訪問到,那么認為他是線程私有的,同步加鎖也就沒有必要做了。 

 

什么是鎖粗化?

 原則上,我們在編寫代碼的時候,總是推薦將同步塊的作用范圍限制的盡量小,僅僅在共享數據的實際作用域才進行同步,這樣是為了使得需要同步的操作盡可能變小,如果存在鎖競爭,那等待鎖的線程也能盡快的拿到鎖。大部分情況下,這兒原則是正確的,但是如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至鎖出現在循環體內,即使沒有線程競爭,頻繁的進行互斥操作也會導致不必要的性能損耗。

 

什么是偏向鎖?

在JDK1.之后引入的一項鎖優化,目的是消除數據在無競爭情況下的同步原語。進一步提升程序的運行性能。 偏向鎖就是偏心的偏,意思是這個鎖會偏向第一個獲得他的線程,如果接下來的執行過程中,改鎖沒有被其他線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。偏向鎖可以提高帶有同步但無競爭的程序性能,也就是說他並不一定總是對程序運行有利,如果程序中大多數的鎖都是被多個不同的線程訪問,那偏向模式就是多余的,在具體問題具體分析的前提下,可以考慮是否使用偏向鎖。

 

關於輕量級鎖?

為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6里鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率,下文會詳細分析

 

什么場景下適合volatile?

volatile能夠實現可見性,但是無法保證原子性。可見性指一個線程修改了這個變量的指,新值對於其他線程來說是可以立即得知的。而普通變量是不能做到這一點的,變量值在線程間傳遞均需要通過主內存完成。volatile的變量在各個線程的工作內存中不存在一致性問題(各個線程的工作內存中volatile變量也可以存在不一致的情況,但是由於每次使用之前都要先刷新,執行引擎看不到不一致的情況,因此可以認為不存在一致性問題)但是java里面的運算並非原子操作的,導致volatile變量運算在並發下一樣是不安全的。

 

什么是CAS?

CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那么處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該 位置的值。Java中通過Unsafe來實現了CAS。

 

如何實現互斥同步?

java中最基本的互斥就是synchronized關鍵字,synchronized在經過編譯后,會在同步塊的前后分別形成monitorenter和moitorexit這兩個字節碼指令。在執行monitorenter指令時,首先要去嘗試獲取對象的鎖,如果這個對象沒有被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1,相應的,在執行monitorexit指令時會把鎖計數器減1,當計數器為0時,鎖就被釋放了。如果獲取對象的鎖失敗,當當前線程就要阻塞等待,知道對象的鎖被另一個線程釋放為止。synchronized對於同一個線程來說是可重入的,不會出現自己把自己鎖死的問題。除了synchronized指望,JUC中的Lock也能實現互斥同步,ReentrantLock,寫法上更加可見,lock和unlock配合try/finally來配合完成,ReentrantLock比synchronized有幾個高級的特性。

 

ReentrantLock的高級特性有那幾個?

1、等待可中斷,當持有鎖的線程長期不釋放的時候,正在等待的線程可以選擇放棄等待,改為處理其他事情;

2、可以實現公平鎖,公平鎖指多個線程在等待同一個鎖時,必須按照申請鎖的順序依次獲得鎖,synchronized是非公平鎖,ReentrantLock默認也是非公平的,只不過可以通過構造函數來制定實現公平鎖;

3、鎖綁定多個條件,ReentrantLock對象可以同時綁定多個Condition對象,在synchronized中,鎖對象的wait/notify/notifyall方法可以實現一個隱含的條件,如果要多一個條件關聯的時候,就需要額外的增加一個鎖;

 

關於鎖的幾個使用建議?

1、使用並發包中的類,並發包中的類大多數采用了lock-free等算法,減少了多線程情況下的資源的鎖競爭,因此對於線程間的共享操作的資源而言,應盡量使用並發包中的類來實現;

2、盡可能少用鎖,沒必要用鎖的地方就不要用了;

3、拆分鎖,即把獨占鎖拆分為多把鎖(這個不一定完全適用);

4、去除讀寫操作的互斥鎖,在修改時加鎖,並復制對象進行修改,修改完畢之后切換對象的引用,而讀取是則不加鎖,這種方式成為CopyOnWrite,CopyOnWriteArrayList就是COW的典型實現,可以明顯提升讀的性能; 

 

關於sunchronized的幾個注意點?

1、當一個線程訪問object的一個synchronized(this)同步代碼塊時, 另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊;

2、當兩個並發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時, 一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以后才能執行該代碼塊;

3、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時, 其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞;

4、Java中的每一個對象都可以作為鎖,對於同步方法,鎖是當前實例對象,對於靜態同步方法,鎖是當前對象的Class對象,對於同步方法塊,鎖是Synchonized括號里配置的對象;

代碼地址:https://github.com/iamzhongyong/about_java_lock

 

參考書籍

《分布式java應用基礎和實踐》

《深入java虛擬機》

《java並發編程實踐》

http://www.infoq.com/cn/articles/java-se-16-synchronized


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM