Java中鎖有多重分類方式,根據粒度可分爲:重量鎖、輕量鎖、偏向鎖、分段鎖;根據鎖獲取公平性又分爲:公平鎖、非公平鎖。根據策略又分爲:樂觀鎖、悲觀鎖、自旋鎖;根據不一樣的分類還有:共享鎖、獨佔鎖、可重入鎖、互斥鎖等概念。java
Synchronized 是經過對象內部的一個叫作監視器鎖(monitor)來實現的。可是監視器鎖本質又是依賴於底層的操做系統的 Mutex Lock 來實現的。而操做系統實現線程之間的切換這就須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized 效率低的緣由。程序員
所以,這種依賴於操做系統 Mutex Lock 所實現的鎖咱們稱之爲
「重量級鎖」。JDK 中對 Synchronized 作的種種優化,其核心都是爲了減小這種重量級鎖的使用。JDK1.6 之後,爲了減小得到鎖和釋放鎖所帶來的性能消耗,提升性能,引入了「輕量級鎖」和「偏向鎖」。安全
「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的。可是,首先須要強調一點的是,輕量級鎖並非用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖。多線程
隨着鎖的競爭,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖(可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。併發
Hotspot 的做者通過以往的研究發現大多數狀況下鎖不只不存在多線程競爭,並且老是由同一線程屢次得到。偏向鎖的目的是在某個線程得到鎖以後,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程獲得了偏護。引入偏向鎖是爲了在無多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑,由於輕量級鎖的獲取及釋放依賴屢次 CAS 原子指令,而偏向鎖只須要在置換ThreadID 的時候依賴一次 CAS 原子指令(因爲一旦出現多線程競爭的狀況就必須撤銷偏向鎖,因此偏向鎖的撤銷操做的性能損耗必須小於節省下來的 CAS 原子指令的性能消耗)。上面說過,輕量級鎖是爲了在線程交替執行同步塊時提升性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提升性能。框架
這並不是一中實際的鎖,而是一中思想、或者說是一種策略;ConcurrentHashMap 是學習分段鎖的最好實踐。經過segment加鎖,增長併發度、下降鎖粒度。jvm
1.加鎖是須要耗費性能和資源的,因此只在有併發安全要求的地方加鎖,減小性能消耗,減小鎖持有時間。
2.下降鎖粒度。大對象會有不少對象進行訪問,因此將其細分爲多個小對象,這樣能夠減低粒度,增長併發度,ConcurrentHashMap就是個很好的例子。
3.鎖分離,最明顯的作法就是ReadWriteLock.根據操做功能進行分離,分紅讀鎖與寫鎖。那麼讀寫、寫寫都是互斥,而讀讀則是共享的,由於不會影響彼此結果。
4.一般狀況下,爲了保證多線程間的有效併發,會要求每一個線程持有鎖的時間儘可能短,即在使用完公共資源後,應該當即釋放鎖。可是,凡事都有一個度,若是對同一個鎖不停的進行請求、同步和釋放,其自己也會消耗系統寶貴的資源,反而不利於性能的優化 。
5.鎖消除是在編譯器級別的事情。在即時編譯器時,若是發現不可能被共享的對象,則能夠消除這些對象的鎖操做,多數是由於程序員編碼不規範引發。ide
獨佔鎖模式下,每次只能有一個線程能持有鎖,ReentrantLock 就是以獨佔方式實現的互斥鎖。獨佔鎖是一種悲觀保守的加鎖策略,它避免了讀/讀衝突,若是某個只讀線程獲取鎖,則其餘讀線程都只能等待,這種狀況下就限制了沒必要要的併發性,由於讀操做並不會影響數據的一致性。因此前面又說道ReadWriteLock進行鎖分離,解決不要的併發問題。函數
共享鎖則容許多個線程同時獲取鎖,併發訪問 共享資源,如:ReadWriteLock。共享鎖則是一種樂觀鎖,它放寬了加鎖策略,容許多個執行讀操做的線程同時訪問共享資源。讀讀是共享的,也是安全的。性能
所謂的公平是指在鎖等待隊列中獲取到鎖前後關係,先到先得的思想。優先隊列中的線程獲取鎖。
加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待
爲了提升性能,Java 提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,若是沒有寫鎖的狀況下,讀是無阻塞的,在必定程度上提升了程序的執行效率。讀寫鎖分爲讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由 jvm 本身控制的,你只要上好相應的鎖便可。
若是你的代碼只讀數據,能夠不少人同時讀,但不能同時寫,那就上讀鎖。
若是你的代碼修改數據,只能有一我的在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
Java 中讀寫鎖有個接口 java.util.concurrent.locks.ReadWriteLock ,也有具體的實現
ReentrantReadWriteLock。
樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採起在寫時先讀出當前版本號,而後加鎖操做(比較跟上一次的版本號,若是同樣則更新),若是失敗則要重複讀-比較-寫的操做。
java 中的樂觀鎖基本都是經過 CAS 操做實現的,CAS 是一種更新的原子操做,比較當前值跟傳入值是否同樣,同樣則更新,不然失敗。
悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,因此每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會 block 直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如 RetreenLock。
自旋鎖原理很是簡單,若是持有鎖的線程能在很短期內釋放鎖資源,那麼那些等待競爭鎖
的線程就不須要作內核態和用戶態之間的切換進入阻塞掛起狀態,它們只須要等一等(自旋),等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。
線程自旋是須要消耗 cup 的,說白了就是讓 cup 在作無用功,若是一直獲取不到鎖,那線程也不能一直佔用 cup 自旋作無用功,因此須要設定一個自旋等待的最大時間。若是持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會致使其它爭用鎖的線程在最大等待時間內仍是獲取不到鎖,這時爭用線程會中止自旋進入阻塞狀態。
自旋鎖儘量的減小線程的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間很是短的代碼塊來
說性能能大幅度的提高,由於自旋的消耗會小於線程阻塞掛起再喚醒的操做的消耗,這些操做會致使線程發生兩次上下文切換!
可是若是鎖的競爭激烈,或者持有鎖的線程須要長時間佔用鎖執行同步塊,這時候就不適合
使用自旋鎖了,由於自旋鎖在獲取鎖前一直都是佔用 cpu 作無用功,佔着 XX 不 XX,同時有大量線程在競爭一個鎖,會致使獲取鎖的時間很長,線程自旋的消耗大於線程阻塞掛起操做的消耗,其它須要 cup 的線程又不能獲取到 cpu,形成 cpu 的浪費。因此這種狀況下咱們要關閉自旋鎖;
自旋鎖的目的是爲了佔着 CPU 的資源不釋放,等到獲取到鎖當即進行處理。可是如何去選擇自旋的執行時間呢?若是自旋執行時間太長,會有大量的線程處於自旋狀態佔用 CPU 資源,進而會影響總體系統的性能。所以自旋的週期選的額外重要!
JVM 對於自旋週期的選擇,jdk1.5 這個限度是必定的寫死的,在 1.6 引入了適應性自旋鎖,適應性自旋鎖意味着自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本認爲一個線程上下文切換的時間是最佳的一個時間,同時 JVM 還針對當前 CPU 的負荷狀況作了較多的優化,若是平均負載小於 CPUs 則一直自旋,若是有超過(CPUs/2)個線程正在自旋,則後來線程直接阻塞,若是正在自旋的線程發現 Owner 發生了變化則延遲自旋時間(自旋計數)或進入阻塞,若是 CPU 處於節電模式則中止自旋,自旋時間的最壞狀況是 CPU的存儲延遲(CPU A 存儲了一個數據,到 CPU B 得知這個數據直接的時間差),自旋時會適當放棄線程優先級之間的差別。
JDK1.6 中-XX:+UseSpinning 開啓;
-XX:PreBlockSpin=10 爲自旋次數;
JDK1.7 後,去掉此參數,由 jvm 控制;
本文裏面講的是廣義上的可重入鎖,而不是單指 JAVA 下的 ReentrantLock。可重入鎖,也叫作遞歸鎖,指的是同一線程 外層函數得到鎖以後 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。在 JAVA 環境下 ReentrantLock 和 synchronized 都是 可重入鎖。
synchronized 它能夠把任意一個非 NULL 的對象看成鎖。他屬於獨佔式的悲觀鎖,同時屬於可重
入鎖。
Synchronized 核心組件
1) Wait Set:哪些調用 wait 方法被阻塞的線程被放置在這裏;
2) Contention List:競爭隊列,全部請求鎖的線程首先被放在這個競爭隊列中;
3) Entry List:Contention List 中那些有資格成爲候選資源的線程被移動到 Entry List 中;
4) OnDeck:任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被成爲 OnDeck;
5) Owner:當前已經獲取到所資源的線程被稱爲 Owner;
6) !Owner:當前釋放鎖的線程。