不止一次的提到過,synchronized是Java內置的機制,是JVM層面的,而Lock則是接口,是JDK層面的
儘管最初synchronized的性能效率比較差,可是隨着版本的升級,synchronized已經變得原來越強大了
這也是爲何官方建議使用synchronized的緣由
畢竟,他是一個關鍵字啊,這纔是親兒子,Lock,終歸差了一點
簡單看下,synchronized大體都通過了哪些重要的變革
重量級鎖
對於最原始的synchronized關鍵字,鎖被稱之爲重量級鎖
由於底層依賴監視器,監視器又依賴操做系統底層的互斥鎖,java的線程是內核映射的
若是獲取不到鎖,那麼就必然會發生內核態與用戶態的轉換,成本很高,因此效率比較低
全部的優化,其實都是爲了將原來的重量級鎖的「重量」變輕...
在如今的版本中,鎖的狀態總共有四種:
很顯然鎖的「重量」從左到右,依次遞增
無所狀態很好理解,新增長的偏向鎖與輕量級鎖,其實就是儘量的將重量級鎖往「無鎖」的方向靠攏,儘量的減小重量
減小重量的思路就是,經過必定的邏輯處理與判斷,若是不須要加鎖,那麼我就少加一點鎖
繼續以前先介紹兩個概念,Mark Word和CAS
Mark Word
對於每一個對象,都有一個對象頭,這部分存儲了對象的必要信息
對象頭中有一個主要區域被稱之爲Mark Word(其實就是那一段地址保存的數據的統稱)
其實能夠簡單理解爲一個數據結構,裏面保存了一些必要的數據信息
爲了節省空間,並非每一個字段都有空間,不一樣的鎖狀態,有不一樣的字段含義,好比說32位,這幾位作什麼,那幾位作什麼,瞭解過class字節碼文件的話應該很容易理解這種思惟
與本文相關的有下面這些,暫時能夠不去思考如何保存的問題,就只須要知道有這麼些字段便可(你簡單理解爲一個結構體,每一個字段都有空間表示也不影響理解此處的敘述)
- 鎖標誌位(什麼類型的鎖),他的標誌包括:無鎖、輕量級鎖、重量級鎖、偏向鎖
- 輕量級鎖時會記錄:指向棧中鎖記錄的指針
- 重量級鎖時會記錄:指向互斥量(重量級鎖)的指針
- 偏向鎖時會記錄:線程ID
CAS
再簡單提一個概念CAShtml
compareAndSwap,比較並替換,是一種實現併發算法時經常使用到的技術java
CAS須要有3個操做數:內存地址V,舊的預期值A,即將要更新的目標值B算法
好比你要操做一個變量,他的值爲A,你但願將他修改成B,這期間不會進行加鎖,當你在修改的時候,你發現值仍舊是A,而後將它修改成B,若是此時值被其餘線程修改了,變成了C,那麼將不會進行值B的寫入操做,這就是CAS的核心理論,經過這樣的操做能夠實現邏輯上的一種「加鎖」,避免了真正去加鎖 安全
輕量級鎖
輕量級鎖本質是藉助於CAS操做,對於競爭不激烈的場景下,能夠減小重量級鎖的使用
線程須要訪問同步代碼體時,會判斷當前狀態是不是無鎖狀態
若是無鎖,將會嘗試經過CAS操做,複製一份Mark Down 而且將Mark Down中的字段指向該線程棧中鎖記錄的指針
- 成功說明沒有競爭,那麼執行同步代碼體;
- 失敗說明存在競爭,那麼鎖會升級爲重量級鎖,Mark Down字段修改成指向重量級鎖指針,而且請求鎖的線程會被阻塞
當持有線程執行結束後,會嘗試藉助於CAS操做恢復Mark Down
若是有競爭會升級爲重量級鎖,Mark Down字段會被修改,CAS操做會失敗,因此:
- 若是這次CAS成功,鎖釋放完成;
- 若是失敗,將會釋放鎖而且喚醒被阻塞的線程
簡要邏輯圖以下
對於輕量級鎖,核心就是CAS操做,由於一旦出現競爭,Mark Down信息將會被修改,而CAS操做的原理就是新值與舊值的對比後再操做,因此CAS操做的成功與否,能夠推斷是否有競爭
有競爭那麼就會升級爲重量級鎖,其餘請求線程會被阻塞,該線程執行結束後會喚醒其餘阻塞線程;不然無競爭就會釋放退出
很顯然,若是競爭激烈的場景,很快就會升級爲重量級鎖,而關於輕量級鎖全部的一切都是徒勞的
不過幸運的是,實踐代表,大多數場景並不會競爭激烈
偏向鎖
對於輕量級鎖中,須要對Mark Down字段進行維護,以及複製Mark Down,以及屢次CAS操做
可是事實上,很多場景中,也的確常常存在只有一個線程訪問的狀況
若是隻有一個線程來回訪問,那麼輕量級鎖的維護相對來講不也是沒有必要的麼,是否還能夠進一步優化?偏向鎖就是一種優化方案
偏向鎖的提出就是針對於這種場景:
鎖不只不存在多線程競爭,並且老是由同一線程屢次得到
因此偏向的概念,就是偏向這個線程,它的核心思想就是:
鎖會偏向第一個獲取它的線程,若是不存在競爭,只有一個線程,則持有偏向鎖的線程永遠不須要同步
若是沒有競爭,能夠看到出來,偏向鎖的能夠約等因而無鎖的
核心原理:
當一個線程訪問同步塊並獲取鎖時,會記錄存儲鎖偏向的線程ID
後續該線程在進入和退出同步塊時再也不須要CAS操做來加鎖和解鎖,只需簡單地判斷一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖
一個簡要的邏輯以下圖所示
ps:
上圖中若是線程ID不是當前線程的話,也會繼續進行CAS操做的,一旦CAS失敗,纔會須要升級,若是成功了,仍是執行同步代碼塊
對於偏向鎖,核心針對一般只有一個線程執行同步代碼的場景
經過記錄偏向鎖ID,對於同一線程,若是無鎖狀態獲取偏向鎖成功或者是偏向鎖,且爲該線程,後續的進出無需額外的同步操做:
可是一旦出現競爭,那麼就會進行鎖升級,升級爲輕量級鎖
小結
輕量級鎖和偏向鎖都是藉助於CAS,若是出現問題,將會進行鎖的升級,升級是不可逆的,也就是說只能從低到高,也就是偏向-->輕量級-->重量級,不可以降級
偏向鎖是對於輕量級鎖的更進一步優化,固然這是有前提的,那就是「場景」
很顯然,對於偏向鎖和輕量級鎖,若是不是同一線程或者線程競爭激烈,將會迅速的從偏向鎖升級爲輕量級鎖,而後迅速變爲重量級鎖,而偏向鎖和輕量級鎖帶來的一切開銷,都將是額外的開銷,因此兩者的開啓與否要根據業務來,不一樣版本的JDK開啓狀態有所不一樣
自旋,適應性自旋
在鎖分類一文中,對自旋的概念進行了簡單介紹
再次說下,所謂自旋,不是獲取不到就阻塞,而是在原地等待一下子,再次嘗試(固然次數或者時長有限),他是以犧牲CPU爲代價來換取內核狀態切換帶來的開銷
適應性自旋是針對於自旋的限制,好比次數(時長)的一種優化,若是本次成功下次多等待幾回,若是常常失敗,可能就不自旋了
藉助於適應性自旋,能夠在CPU時間片的損耗和內核狀態的切換開銷之間相對的找到一個平衡,進而可以提升性能
從原來的一旦獲取不到就阻塞、狀態切換,轉變爲在有的時候能夠藉助於較小的CPU浪費避免狀態切換的開銷,因此顯然能夠提高性能
鎖消除
鎖消除,就是消除鎖
但是,難道好好地一個synchronized方法,最後JVM會把關鍵字去掉嗎?顯然不是這個意思
他指的是刪除非必要的同步
根據代碼逃逸技術,若是判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼能夠認爲這段代碼是線程安全的,沒必要要加鎖
什麼是逃逸,好比A方法,調用B方法,B方法將內部建立的一個局部對象,返回給了A,那麼這個B中的變量就屬於逃逸了,就存在被其餘線程訪問的可能
簡單說除了你寫代碼以外,Java底層包括從編譯器到JVM還有不少工做人員在忙活,人家經過算法一看,你這根本就沒有必要使用同步,就會在實際執行的時候把你的同步去掉
你可能覺得,我本身哪有寫不少synchronized修飾的方法?
可是你仔細想一下,你即便不寫,代碼中JDK提供的方法、別人提供的Jar包中的方法,他們用了多少synchronized?
最終不都會進入到你的方法中麼?
因此實際代碼中的synchronized(同步)遠比你想象到的要多得多
因此若是能夠消除沒必要要的同步,豈不是性能會有所提高?
鎖粗化(鎖膨脹)
仍是以方法調用爲例,假如一個A方法,中有三個對象b,c,d,分別調用他們的方法並且都是同步方法
void A(){
b.function();
c.function();
d.function();
}
每一個方法都加鎖解鎖,是否是很煩很費電?
若是碰巧他們用的都是同一把鎖呢?是否是能夠嘗試進行合併?
也就是說,虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操做,就會將其合併成一次範圍更大的加鎖和解鎖操做
多個加鎖解鎖操做,轉變爲一次的加鎖和解鎖
加鎖解鎖必然會消耗性能,若是能夠進行合併,顯然能夠提升性能
小結
以上爲大體的synchronized優化過的點
面對一個蒸蒸日上的努力小青年,並且還有那麼多自身具備的優點(隱式鎖相對於顯式鎖,對開發者來講友好了不少,畢竟若是能夠,你們都不喜歡多操心的)
有什麼理由必定要放棄他呢?
因此除非場景特殊,或者對程序分析後,業務適合,不然儘量的選擇synchronized吧
synchronized是重量級的,他能夠「包治百病」,儘管性能或許沒有你的指望那麼好
另外有些優化好比偏向鎖、輕量級鎖,對場景是有要求的,若是無論三七二十一,也許並不必定會提升,反而更差,因此對於鎖優化參數的開閉也須要參照場景