synchronized只會用不知道原理?一文搞定

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"聽說微信搜索《Java魚仔》會變更強哦!","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文收錄於","attrs":{}},{"type":"link","attrs":{"href":"https://github.com/OliverLiy/JavaStarter","title":""},"content":[{"type":"text","text":"JavaStarter","attrs":{}}]},{"type":"text","text":",裏面有我完整的Java系列文章,學習或面試都可以看看哦","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"(一)概述","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在多線程的程序執行中,有可能會出現多個線程會同時訪問一個共享並且可變資源的情況,這種時候由於線程的執行是不可控的,所以必須採用一些方式來控制該資源的訪問,這種方式就是“加鎖”。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們把那些可能會被多個線程同時操作的資源稱爲臨界資源,加鎖的目的就是讓這些臨界資源在同一時刻只能有一個線程可以訪問。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"(二)CAS的介紹","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"CAS:compare and swap","attrs":{}},{"type":"text","text":",比較且交換。使用CAS操作可以在沒有鎖的情況下完成多線程對一個值的更新。CAS的具體操作如下:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當要更新一個值時,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"先獲取當前值E,計算更新後的結果值V","attrs":{}},{"type":"text","text":"(先不更新),當要去更新這個值時,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"比較此時這個值是否還是等於E,如果相等,則將E更新爲V,如果不相等,則重新進行上面的操作","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以i++操作爲例,在沒有鎖的情況下,這個操作是線程不安全的,假設i的初始值爲0,CAS操作先獲取原值E=0,計算更新後的值V=1,要更新之前先比較這個值是否還是等於0,如果等於0則將E更新爲1,如果不等於0則說明有線程已經更新了,重新獲取E值=1,繼續執行。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"ABA問題","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CAS操作可能會出現ABA問題,ABA問題即我們要去比較的這個值E,經過多個線程的操作後從0變成1又變成了0。此時雖然E值和更新前相等,但是還是已經被更新了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"ABA問題的解決辦法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對E值增加一個版本號,每次要獲取數據時將版本號也獲取,每次更新完數據之後將版本號遞增,這樣就算值相等通過版本號也能知道是否經過修改。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"java在很多地方都用到了CAS操作,比如Atomic的一些類:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"AtomicInteger i=new AtomicInteger();","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進入AtomicInteger方法中,可以看到有個叫Unsafe的類,進入這個類中,可以看到CAS的幾個操作方法","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a5/a5a083165443da6c1a159a0caf18b2fb.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"(三)對象在內存中的存儲佈局","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要想學會synchronized,首先要理解Java對象的內存佈局,或者稱爲內存結構。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e1/e1a8bb76f3d745a2b84ba6c7c9891109.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個對象分爲對象頭、實例數據和對其填充。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"其中對象頭Header佔12個字節","attrs":{}},{"type":"text","text":":Mark Word佔8個字節,類型指針class pointer佔4個字節(默認經過了壓縮,如果不開啓壓縮佔8個字節)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"實例對象","attrs":{}},{"type":"text","text":"按實際存儲有不同大小,對象爲空時等於0。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"Padding","attrs":{}},{"type":"text","text":"表示對齊,當此時內存所佔字節不能被8整除時補上相應字節數。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以Object o=new Object()爲例,我們先導入一個jol依賴,通過jol可以看到具體的內存佈局","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"xml"},"content":[{"type":"text","text":"\n org.openjdk.jol\n jol-core\n 0.9\n","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"運行以下代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public static void main(String[] args) {\n Object o=new Object();\n System.out.println(ClassLayout.parseInstance(o).toPrintable());\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"觀察結果,OFFSET表示偏移量的起始點,SIZE表示所佔字節,前兩行是Mark Word一共佔8個字節,第三行是class pointer佔4個字節,此時對象爲空,實例對象等於0,最後padding補齊,一共16個字節。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c8/c882397f1ce1558dce4ef6095da8840a.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"(三)synchronized","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"synchronized可以保證在同一時刻,只有一個線程可以執行某個方法或某個代碼塊,synchronized把鎖信息存放在對象頭的MarkWord中。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"synchronized作用在非靜態方法上是對方法的加鎖,synchronized作用在靜態方法上是對當前的類加鎖。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在早期的jdk版本中,synchronized是一個重量級鎖,保證線程的安全但是效率很低。後來對synchronized進行了優化,有了一個","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"鎖升級的過程","attrs":{}},{"type":"text","text":":","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"無鎖態(new)-->偏向鎖-->輕量級鎖(自旋鎖)-->重量級鎖","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過MarkWord中的8個字節也就是64位來記錄鎖信息。也有人將自旋鎖稱爲無鎖,因爲自選操作並沒有給一個對象上鎖,這裏只要理解意思即可。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/62/6283d59aa2a138d526c47031537b5bfa.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3.1 鎖升級過程詳解:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當給一個對象增加synchronized鎖之後,相當於上了一個","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"偏向鎖","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當有一個線程去請求時,就把這個對象MarkWord的ID改爲當前線程指針ID(JavaThread),只允許這一個線程去請求對象。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"當有其他線程也去請求時,就把鎖升級爲輕量級鎖","attrs":{}},{"type":"text","text":"。每個線程在自己的線程棧中生成LockRecord,用CAS自旋操作將請求對象MarkWordID改爲自己的LockRecord,成功的線程請求到了該對象,未成功的對象繼續自旋。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果競爭加劇,當有線程自旋超過一定次數時(在JDK1.6之後,這個自旋次數由JVM自己控制),","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"就將輕量級鎖升級爲重量級鎖","attrs":{}},{"type":"text","text":",線程掛起,進入等待隊列,等待操作系統的調度。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3.2 加鎖的字節碼實現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"synchronized關鍵字被編譯成字節碼之後會被翻譯成monitorenter和monitorexit兩條指令,進入同步代碼塊時執行monitorenter,同步代碼塊執行完畢後執行monitorexit","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"(四)鎖消除","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在某些情況下,如果JVM認爲不需要鎖,會自動消除鎖,比如下面這段代碼:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public void add(String a,String b){\n StringBuffer sb=new StringBuffer();\n sb.append(a).append(b);\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"StringBuffer是線程安全的,但是在這個add方法中stringbuffer是不能共享的資源,因此加鎖只會徒增性能消耗,JVM就會消除StringBuffer內部的鎖。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"(五)鎖粗化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在某些情況下,JVM檢測到一連串的操作都在對同一個對象不斷加鎖,就會將這個鎖加到這一連串操作的外部,比如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"StringBuffer sb=new StringBuffer();\nwhile(i<100){\n sb.append(str);\n i++;\n}","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述操作StringBuffer每次添加數據都要加鎖和解鎖,連續100次,這時候JVM就會將鎖加到更外層(while)部分。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"(六)逃逸分析","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先問一個經常基礎的虛擬機問題,實例對象存放在虛擬機的哪個位置?按以前的回答,示例對象放在堆上,引用放在棧上,示例的元數據等存放在方法區或者元空間。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但這是有前提的,前提是示例對象沒有線程逃逸行爲。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JDK1.7開始默認開啓了逃逸分析,所謂逃逸分析,就是指如果一個對象被編譯器發現只能被一個線程訪問,那麼這個對象就不需要考慮同步。JVM就對這種對象進行優化,將堆分配轉化爲棧分配,歸根結底就是虛擬機在編譯過程中對程序的一種優化行爲。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"開啓逃逸分析:­ XX:+DoEscapeAnalysis\n關閉逃逸分析: ­XX:­-DoEscapeAnalysis","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章