java中的鎖機制

在java中的鎖分爲以下(其實就是按照鎖的特性和設計來劃分):

1、公平鎖/非公平鎖

2、可重入鎖

3、獨享鎖/共享鎖

4、互斥鎖/讀寫鎖

5、樂觀鎖/悲觀鎖

6、分段鎖

7、偏向鎖/輕量級鎖/重量級鎖

8、自旋鎖(java.util.concurrent包下的幾乎都是利用鎖)

從底層角度看常見的鎖也就兩種:Synchronized和Lock接口以及ReadWriteLock接口(讀寫鎖)

從類關係看出Lock接口是jdk5後新添的來實現鎖的功能,其實現類:ReentrantLock、WriteLock、ReadLock。

其實還有一個接口ReadWriteLock,讀寫鎖(讀讀共享、讀寫獨享、寫讀獨享、寫寫獨享)

Lock接口與synchronized關鍵字本質上都是實現同步功能。

區別:ReentrantLock:使用上需要顯示的獲取鎖和釋放鎖,提高可操作性、可中斷的獲取獲取鎖以及可超時的獲取鎖,默認是                                          非公平的但可以實現公平鎖,悲觀,獨享,互斥,可重入,重量級鎖。

           ReentrantReadWriteLock:默認非公平但可實現公平的

,悲觀,寫獨享,讀共享,讀寫,可重入,重量級鎖。

           synchronized:關鍵字,隱式的獲取鎖和釋放鎖,不具備可中斷、可超時,非公平、互斥、悲觀、獨享、可重入的重量級

Lock的使用也很簡單:

  1. Lock lock = new ReentrantLock();
  2. lock.lock();
  3. try{
  4. }finally{
  5. lock.unlock();
  6. }
  7. //注意:不要將lock方法寫在try塊中,因爲如果在獲取鎖的時候發生異常,異常拋出的同時也會導致鎖無故的釋
  8. //放 否則會程序會報監視狀態異常
  9. Exception in thread "線程一" java.lang.IllegalMonitorStateException
  10. at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155)
  11. at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260)
  12. at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460)
  13. //ReentrantLock必須要在finally中unlock(), 否則,如果在被加鎖的代碼中拋出了異常,那麼這個鎖將會永遠無法釋放.
  14. //synchronized就沒有這樣的問題, 遇到異常退出時,會釋放掉已經獲得的鎖.

Lock接口提供的 ,synchronized關鍵字所不具備的特性

特性 描述
嘗試性非阻塞地獲取鎖(tryLock方法) 當前線程嘗試的獲取鎖,如果這一時段沒有被q其他線程獲取,則成功的獲取鎖,否則直接返回false
能被中斷的獲取鎖(lockInterruptibly()throws InterruptedException)

與synchronized不同,獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷時,中斷異常將會被拋出,同時鎖會被釋放。

		<p><strong>兩種情況:</strong></p>

		<p><strong>①:當前線程獲取鎖之前(並未參與獲取鎖)被其他線程標記interrupt中斷,當調用此方法時直接拋出中斷異常。</strong></p>

		<p><strong>②:當前線程獲取鎖,並且鎖被其他線程持有,則一直阻塞,此時其他線程來中斷此線程,則會拋出中斷異常。</strong></p>
		</td>
	</tr><tr><td style="border-color:#33ccff;width:243px;"><strong>超時獲取鎖(tryLock(long time,TimeUtil unit)throws InterruptedException)</strong></td>
		<td style="border-color:#33ccff;width:605px;">
		<p><strong>在指定的時間內能夠獲取鎖,超出時間仍熱無法獲取,則返回</strong></p>

		<p><strong>會有以下3種情況:</strong></p>

		<p><strong>①:當前線程在指定時間內獲取了鎖。</strong></p>

		<p><strong>②:當前線程在指定時間內被中斷,鎖被釋放。</strong></p>

		<p><strong>3:當前線程在超出指定的時間,則直接返回false。</strong></p>
		</td>
	</tr></tbody></table></div><p>以下測試代碼,測試</p>
  1. Lock lock = new ReentrantLock();
  2. final MMT m = new MMT(lock);
  3. Thread tt = new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. System.out.println("線程一 開始執行。。。");
  7. try {
  8. m.update("張三");
  9. } catch (InterruptedException e) {
  10. System.out.println(Thread.currentThread().getName()+"被中斷(鎖釋放)。。。");
  11. }
  12. System.out.println("線程一 結束執行。。。");
  13. }
  14. },"線程一");
  15. Thread tt2 = new Thread(new Runnable() {
  16. @Override
  17. public void run() {
  18. System.out.println("線程二 開始執行。。。");
  19. try {
  20. m.update("李四");
  21. } catch (InterruptedException e) {
  22. // TODO Auto-generated catch block
  23. System.out.println(Thread.currentThread().getName()+"被中斷(鎖釋放)。。。");
  24. }
  25. System.out.println("線程二 結束執行。。。");
  26. }
  27. },"線程二");
  28. tt.start();
  29. tt2.start();
  30. //中斷線程
  31. tt.interrupt();
  32. try {
  33. tt.join();
  34. tt2.join();
  35. } catch (InterruptedException e) {
  36. // TODO Auto-generated catch block
  37. e.printStackTrace();
  38. }
  39. }
  40. class MMT {
  41. String name;
  42. Lock lock=null;
  43. public MMT(Lock lock) {
  44. this.lock=lock;
  45. }
  46. public void update(String name) throws InterruptedException{
  47. // lock.lock();
  48. // boolean tryLock = lock.tryLock();//嘗試獲取鎖
  49. //中斷只是在當前線程獲取鎖之前,或者當前線程獲取鎖的時候被阻塞
  50. // lock.lockInterruptibly();
  51. lock.tryLock(3000, TimeUnit.SECONDS);
  52. try{
  53. setName(name);
  54. System.out.println(Thread.currentThread().getName()+" 變換後的姓名爲"+name);
  55. }finally{
  56. lock.unlock();
  57. }
  58. }
  59. public void setName(String name) {
  60. this.name = name;
  61. }
  62. public String getName() {
  63. return name;
  64. }
  65. }

可實現公平鎖

    對於ReentrantLock而言,可實現公平鎖 ,通過構造函數指定是否需要公平,默認是非公平,區別在與非公平隨機性,並且高併發下吞吐量大,公平的話根據請求鎖等待的時間長短,等待的長了優先,類似FIFO,吞吐量降低了。

鎖綁定多個條件

       指ReentrantLock對象可以同時綁定多個Condition條件對象,而在Synchroized中,鎖對象的wait方法、notify方法、和notifyall方法可以實現一個隱含條件,如果需要多個,得額外的添加一個鎖對象。在ReentrantLock中不需要,只需要創建多個條件對象即可(new Condition()),對應的await()、siganl()、signalAll()。

synchronized的優勢

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

應用場景:

   在資源競爭不激烈的情況下,synchronized關鍵字的性能優與ReentrantLock,相反,ReentrantLock的性能保持常態,優於關鍵字。

按照其性質劃分:

      公平鎖/非公平鎖

       公平鎖指多個線程按照申請鎖的順序來依次獲取鎖。非公平鎖指多個線程獲取鎖的順序並不是按照申請鎖的順序來獲取,有可能後申請鎖的線程比先申請鎖的線程優先獲取到鎖,此極大的可能會造成線程飢餓現象,遲遲獲取不到鎖。由於ReentrantLock是通過AQS來實現線程調度,可以實現公平鎖,,但是synchroized是非公平的,無法實現公平鎖。

  1. /**
  2. * 公平鎖與非公平鎖測試
  3. */
  4. public class FairAndUnFairThreadT {
  5. public static void main(String[] args) throws InterruptedException {
  6. //默認非公平鎖
  7. final Lock lock = new ReentrantLock(true);
  8. final MM m = new MM(lock);
  9. for (int i=1;i<=20 ;i++){
  10. String name = "線程"+i;
  11. Thread tt = new Thread(new Runnable() {
  12. @Override
  13. public void run() {
  14. for(int i=0;i<2;i++){
  15. m.testReentrant();
  16. }
  17. }
  18. },name);
  19. tt.start();
  20. }
  21. }
  22. }
  23. class MM {
  24. Lock lock = null;
  25. MM(Lock lock){
  26. this.lock = lock;
  27. }
  28. public void testReentrant(){
  29. lock.lock();
  30. try{
  31. Thread.sleep(1);
  32. System.out.println(Thread.currentThread().getName() );
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. } finally {
  36. lock.unlock();
  37. }
  38. }
  39. public synchronized void testSync(){
  40. System.out.println(Thread.currentThread().getName());
  41. }
  42. }

但是未必絕對就是按照順序,可能因爲CPU準備原因,可能個別會不是公平的。

  樂觀鎖與悲觀鎖

      不是指什麼具體類型的鎖,而是指在併發同步的角度。悲觀鎖認爲對於共享資源的併發操作,一定是發生xi修改的,哪怕沒有發生修改,也會認爲是修改的,因此對於共享資源的操作,悲觀鎖採取加鎖的方式,認爲,不加鎖的併發操作一定會出現問題。樂觀鎖認爲對於共享資源的併發操作是不會發生修改的,在更新數據的時候,會採用嘗試更新,不斷重試的方式更新數據。樂觀的認爲,不加鎖的併發操作共享資源是沒問題的。從上面的描述看除,樂觀鎖不加鎖的併發操作會帶來性能上的提升,悲觀鎖的使用就是利用synchroized關鍵字或者lock接口的特性。樂觀鎖在java中的使用,是無鎖編程常常採用的是CAS自旋鎖,典型的例子就是併發原子類,通過CAS自旋(spinLock)來更新值。

獨享鎖與共享鎖

    獨享鎖是指該鎖一次只能被一個線程所持有。共享鎖是指可被多個線程所持有。在java中,對ReentrantLock對象以及synchroized關鍵字而言,是獨享鎖的。但是對於ReadWriteLock接口而言,其讀是共享鎖,其寫操作是獨享鎖。讀鎖的共享鎖是可保證併發讀的效率,讀寫、寫寫、寫讀的過程中都是互斥的,獨享的。獨享鎖與共享鎖在Lock的實現中是通過 AQS(抽象隊列同步器)來實現的。

互斥鎖與讀寫鎖

      互斥鎖與讀寫鎖就是具體的實現,互斥鎖在java 中的體現就是synchronized關鍵字以及Lock接口實現類ReentrantLock,讀寫鎖在java中的具體實現就是ReentrantReadWriteLock。

可重入鎖

   又名遞歸鎖,是指同一個線程在外層的方法獲取到了鎖,在進入內層方法會自動獲取到鎖。對於ReentrantLock和synchronized關鍵字都是可重入鎖的。最大的好處就是能夠避免一定程度的死鎖。

  1. public sychrnozied void test() {
  2. //執行邏輯,調用另一個加鎖的方法
  3. test2();
  4. }
  5. public sychronized void test2() {
  6. //執行業務邏輯
  7. }

在上面代碼中,sychronized關鍵字加在類方法上,執行test方法獲取當前對象作爲監視器的對象鎖,然後又調用test2同步方法。

一、如果鎖是可重入的話,那麼當前線程就在調用test2時並不需要再次獲取當前鎖對象,可以直接進入test2方法。

二、如果鎖是不具備可重入的話,那麼該線程在調用test2前會等待當前對象鎖的釋放,實際上該對象鎖已被當前線程所持有不可能再此獲得。那麼就會發生死鎖。

按照設計方案來分類(目的對鎖的進一步優化)

  自旋鎖與自適應自旋鎖(或者說是自旋鎖的變種TicketLock、MCSLock、CLHLock)

底層採用CAS來保證原子性,自旋鎖獲取鎖的時候不會阻塞,而是通過不斷的while循環的方式嘗試獲取鎖。優點:減少線程上下文切換的消耗,缺點是會消耗CPU。如果鎖被佔用的時間很短,自旋等待的效果就會非常好,反之,如果鎖被佔用的時間很長,那麼自旋的線程只會白白消耗處理器資源,而不會做任何有用的工作,反而會帶來性能上的浪費。

偏向鎖、輕量級鎖、重量級鎖

這三種鎖是指鎖的狀態,並且是針對Synchronized,在java通過引入鎖升級的機制來實現高校的synchronized。鎖的狀態是通過對象監視器在對象頭中的字段來表明的。

偏向鎖:指一段同步代碼一直被同一個線程s所訪問,那麼該線程會自動的獲取鎖。降低獲取鎖的代價。

輕量級鎖:當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,                     不會阻塞,提高性能。

重量級鎖:當鎖爲輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒獲取到鎖

                  就會進入阻塞,該鎖膨脹爲重量級鎖。重量級會讓其他申請線程阻塞,性能降低。 

偏向所鎖,輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。
一個對象剛開始實例化的時候,沒有任何線程來訪問它的時候。它是可偏向的,意味着,它現在認爲只可能有一個線程來訪問它,所以當第一個
線程來訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭成爲偏向鎖的時候使用CAS操作,並將
對象頭中的ThreadID改成自己的ID,之後再次訪問這個對象時,只需要對比ID,不需要再使用CAS在進行操作。
一旦有第二個線程訪問這個對象,因爲偏向鎖不會主動釋放,所以第二個線程可以看到對象時偏向狀態,這時表明在這個對象上已經存在競爭了,檢查原來持有該對象鎖的線程是否依然存活,如果掛了,則可以將對象變爲無鎖狀態,然後重新偏向新的線程,如果原來的線程依然存活,則馬上執行那個線程的操作棧,檢查該對象的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級爲輕量級鎖,(偏向鎖就是這個時候升級爲輕量級鎖的)。如果不存在使用了,則可以將對象回覆成無鎖狀態,然後重新偏向。
輕量級鎖認爲競爭存在,但是競爭的程度很輕,一般兩個線程對於同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個線程就會釋放鎖。 但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹爲重量級鎖,重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止CPU空轉。

分段鎖

分段鎖其實是一種鎖的設計,並不是具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是通過分段鎖的形式來實現高效的併發操作。我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)

當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,然後對這個分段進行加鎖,所以當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。

但是,在統計size的時候,可就是獲取hashmap全局信息的時候,就需要獲取所有的分段鎖才能統計。分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操作。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章