做者前面也寫了幾篇關於Java併發編程,以及線程和volatil的基礎知識,有興趣能夠閱讀做者的原文博客,今天關於Java中的兩種鎖進行詳解,但願對你有所幫助java
本文受趙sir原創發佈,轉載請聯繫原創 https://blog.csdn.net/qq_36094018/article/details/90140209面試
在上一章中說了volatile,在多線程下能夠保證變量的可見性,可是不能保證原子性,下面一段代碼說明:編程
運行上面代碼,會發現輸出flag的值不是理想中10000,雖然volatile寫入時候會通知其餘線程的工做內存值無效,從主內存重寫讀取。i++是三步操做,讀取-賦值-寫入不能保證原子性。原子性:不能被中斷要麼成功要麼失敗。數組
好比此時主內存的flag值10,線程1和線程2讀取到本身工做內存都是10,而後線程1在進行賦值的時候,線程2執行了,這時線程2發現本身內存的值和主內存的值同樣,並無修改,而後賦值寫入11,此時線程1運行,由於以前讀過了,會往下繼續運行寫入也是11。那麼兩個線程至關於只增長了一次。要想達到理想值,只須要修改public synchronized void increase() { flag++; }
就好了。安全
Java提供的一種原子性性內置鎖,Java每一個對象均可以把它當作是監視器鎖,線程代碼執行在進入synchronized代碼塊時候會自動獲取內部鎖,這個時候其餘線程訪問時候會被阻塞到隊列,直到進入synchronized中的代碼執行完畢或者拋出異常或者調用了wait方法,都會釋放鎖資源。在進入synchronized會從主內存把變量讀取到本身工做內存,在退出的時候會把工做內存的值寫入到主內存,保證了原子性。多線程
編譯後執行javap -v Test.class就會發現兩條指令。併發
synchronized是使用一種monitor機制,在進入鎖時候先執行monitorenter指令。退出的時候執行monitorexit指令。synchronized是可重入鎖,每一個對象中都含有一個計數器當前線程再次獲取鎖,計數器+1,退出時候計算器-1,直到計數器爲0才釋放鎖資源,喚醒其餘線程來爭搶資源。任意一個對象都擁有本身的監視器,只有在線程獲取到監視器鎖時纔會進入代碼中,不然就進入阻塞狀態。框架
synchronized在1.6之前是重量級鎖,當前只有一個線程執行,其餘線程阻塞。爲了減小得到鎖和釋放鎖帶來的性能問題,而引入了偏向鎖、輕量級鎖以及鎖的存儲過程和升級過程。在1.6後鎖分爲了無鎖、偏向鎖、輕量鎖、重量鎖,鎖的狀態在多線程競爭的狀況下會逐漸升級,只能升級而不能降級,這樣是爲了提升鎖獲取和釋放的效率。性能
synchronized的鎖是存貯在Java對象頭裏的,若是對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。1個字寬等於4個字節。學習
Java對象頭中的Mark Word裏默認存儲了對象是HashCode、分代年齡、和鎖標記。
在運行的時候,Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化,可能會變化爲存儲如下四種形式。
偏向鎖的意思將來只有一個線程使用鎖,不會有其餘線程來爭取。
獲取鎖:
首先檢查Mark word中鎖的標誌是否爲01。
若是是01,判斷對象頭的Mark word記錄是否爲當前線程ID,若是是執行5,不然執行3.
線程ID並未只指向本身,發送CAS競爭,若是競爭成功,則將Mark Word中線程ID設置爲當前線程ID,執行5;若是未成功執行4。
當到達全局安全點(在這個時間點上沒有正在執行的字節碼)時得到偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,而後被阻塞在安全點的線程繼續往下執行同步代碼。
執行同步代碼。 撤銷鎖:偏向鎖使用了一種等到競爭出現才釋放鎖的機制,因此當其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖。須要等待全局安全點,它首先暫停原持有偏向鎖的線程,而後檢查線程是否還在活着,若是線程處於未活動狀態,則釋放鎖標記,若是處於活動狀態則升級爲輕量級鎖。
CAS全稱是Compare And Swap 即比較並交換,使用樂觀鎖機制,包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。 若是內存位置的值與預期原值相匹配,那麼纔會將該位置值更新爲新值 。不然,處理器不作任何操做。
線程在執行同步代碼塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。
加鎖:
解鎖: 輕量級解鎖時,會使用原子的CAS操做將Displaced Mark Word替換回到對象頭,若是成功,則表示沒有競爭發生。若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。
它是在1.5以後提供的一個獨佔鎖接口,它的實現類是ReentrantLock,相比較synchronized這種隱式鎖(不用手動加鎖和釋放鎖)的便捷性,可是提供了更加鎖的可操做性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized不具有的特性。
在finally中釋放鎖,目的保證獲取鎖最終被釋放。不要在獲取鎖寫在try裏,由於若是在獲取鎖時發生了異常,異常拋出的同時,也會致使鎖無端釋放。
AQS是隊列同步器(AbstractQueuedSynchronizer),是用來構建鎖或者其餘同步器的基礎框架,它使用了一個int成員變量表示同步狀態,經過內置的FIFO隊列來完成資源獲取的線程排隊工做問題。AQS在內部維護了一個單一的狀態信息state,能夠經過getState、setState、compareAndSetState(CAS操做)修改此值,對於ReentrantLock來講,state能夠用來表示當前線程獲取鎖的可重入次數。ReentrantLock中當一個線程獲取了鎖,在AQS的內部會進行compareAndSetState將state變爲1,若是再次獲取就設置爲2,釋放鎖也會去修改state值,只有當值變爲0時,其餘線程才能得到鎖。
AQS底層維護state和隊列來實現獨佔和共享兩種鎖。
**獨佔鎖:**每次只能有一個線程能持有鎖,如lock、synchronized。 **共享鎖:**容許多個線程同時獲取鎖,併發訪問共享資源,如ReadWriteLock。
lock分爲公平鎖和非公平鎖,實現了AQS接口,經過FIFO設置鎖的優先級。
**公平鎖:**根據線程獲取鎖的時間來判斷,等待時間越久的線程優先被執行。Lock中初始化的時候ReentrantLock(true),默認爲false,效率較低由於須要判斷線程的等待時間。
**非公平鎖:**搶佔鎖資源,不能保證獲取鎖的線程優先級,效率較高,由於獲取鎖是競爭的。
還記得在Java併發二中有一道生產者消費者,使用的是synchronized+wait(notify),lock中也提供了這種等待通知類型的方法await和signal,當前線程調用這些方法時,須要提早獲取到Condition對象關聯的鎖,Condition是依賴於Lock對象,調用lock對象中的newCondition。
老樣子仍是先定義一個容器:
生產者:啓5個線程往容器裏添加數據。
消費者:啓10線程消費數據
註釋基本明確,就很少說了。wait和notify是配合synchronized使用,await和signal是配合lock使用,區別在於喚醒時notify不能指定線程喚醒,signal能夠喚醒具體的線程,更小的粒度控制鎖。
在這裏得到的不只僅是技術!