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

據說微信搜索《Java魚仔》會變動強哦!java

本文收錄於JavaStarter,裏面有我完整的Java系列文章,學習或面試均可以看看哦git

(一)概述

在多線程的程序執行中,有可能會出現多個線程會同時訪問一個共享而且可變資源的狀況,這種時候因爲線程的執行是不可控的,因此必須採用一些方式來控制該資源的訪問,這種方式就是「加鎖」。github

咱們把那些可能會被多個線程同時操做的資源稱爲臨界資源,加鎖的目的就是讓這些臨界資源在同一時刻只能有一個線程能夠訪問。面試

(二)CAS的介紹

CAS:compare and swap,比較且交換。使用CAS操做能夠在沒有鎖的狀況下完成多線程對一個值的更新。CAS的具體操做以下:安全

當要更新一個值時,先獲取當前值E,計算更新後的結果值V(先不更新),當要去更新這個值時,比較此時這個值是否仍是等於E,若是相等,則將E更新爲V,若是不相等,則從新進行上面的操做微信

以i++操做爲例,在沒有鎖的狀況下,這個操做是線程不安全的,假設i的初始值爲0,CAS操做先獲取原值E=0,計算更新後的值V=1,要更新以前先比較這個值是否仍是等於0,若是等於0則將E更新爲1,若是不等於0則說明有線程已經更新了,從新獲取E值=1,繼續執行。多線程

ABA問題app

CAS操做可能會出現ABA問題,ABA問題即咱們要去比較的這個值E,通過多個線程的操做後從0變成1又變成了0。此時雖然E值和更新前相等,可是仍是已經被更新了。佈局

ABA問題的解決辦法性能

對E值增長一個版本號,每次要獲取數據時將版本號也獲取,每次更新完數據以後將版本號遞增,這樣就算值相等經過版本號也能知道是否通過修改。

java在不少地方都用到了CAS操做,好比Atomic的一些類:

AtomicInteger i=new AtomicInteger();

進入AtomicInteger方法中,能夠看到有個叫Unsafe的類,進入這個類中,能夠看到CAS的幾個操做方法

在這裏插入圖片描述

(三)對象在內存中的存儲佈局

要想學會synchronized,首先要理解Java對象的內存佈局,或者稱爲內存結構。

在這裏插入圖片描述

一個對象分爲對象頭、實例數據和對其填充。

其中對象頭Header佔12個字節:Mark Word佔8個字節,類型指針class pointer佔4個字節(默認通過了壓縮,若是不開啓壓縮佔8個字節)

實例對象按實際存儲有不一樣大小,對象爲空時等於0。

Padding表示對齊,當此時內存所佔字節不能被8整除時補上相應字節數。

以Object o=new Object()爲例,咱們先導入一個jol依賴,經過jol能夠看到具體的內存佈局

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

運行如下代碼:

public static void main(String[] args) {
    Object o=new Object();
    System.out.println(ClassLayout.parseInstance(o).toPrintable());
}

觀察結果,OFFSET表示偏移量的起始點,SIZE表示所佔字節,前兩行是Mark Word一共佔8個字節,第三行是class pointer佔4個字節,此時對象爲空,實例對象等於0,最後padding補齊,一共16個字節。

在這裏插入圖片描述

(三)synchronized

synchronized能夠保證在同一時刻,只有一個線程能夠執行某個方法或某個代碼塊,synchronized把鎖信息存放在對象頭的MarkWord中。

synchronized做用在非靜態方法上是對方法的加鎖,synchronized做用在靜態方法上是對當前的類加鎖。

在早期的jdk版本中,synchronized是一個重量級鎖,保證線程的安全可是效率很低。後來對synchronized進行了優化,有了一個鎖升級的過程

無鎖態(new)-->偏向鎖-->輕量級鎖(自旋鎖)-->重量級鎖

經過MarkWord中的8個字節也就是64位來記錄鎖信息。也有人將自旋鎖稱爲無鎖,由於自選操做並無給一個對象上鎖,這裏只要理解意思便可。

在這裏插入圖片描述

3.1 鎖升級過程詳解:

當給一個對象增長synchronized鎖以後,至關於上了一個偏向鎖

當有一個線程去請求時,就把這個對象MarkWord的ID改成當前線程指針ID(JavaThread),只容許這一個線程去請求對象。

當有其餘線程也去請求時,就把鎖升級爲輕量級鎖。每一個線程在本身的線程棧中生成LockRecord,用CAS自旋操做將請求對象MarkWordID改成本身的LockRecord,成功的線程請求到了該對象,未成功的對象繼續自旋。

若是競爭加重,當有線程自旋超過必定次數時(在JDK1.6以後,這個自旋次數由JVM本身控制),就將輕量級鎖升級爲重量級鎖,線程掛起,進入等待隊列,等待操做系統的調度。

3.2 加鎖的字節碼實現

synchronized關鍵字被編譯成字節碼以後會被翻譯成monitorenter和monitorexit兩條指令,進入同步代碼塊時執行monitorenter,同步代碼塊執行完畢後執行monitorexit

(四)鎖消除

在某些狀況下,若是JVM認爲不須要鎖,會自動消除鎖,好比下面這段代碼:

public void add(String a,String b){
    StringBuffer sb=new StringBuffer();
    sb.append(a).append(b);
}

StringBuffer是線程安全的,可是在這個add方法中stringbuffer是不能共享的資源,所以加鎖只會徒增性能消耗,JVM就會消除StringBuffer內部的鎖。

(五)鎖粗化

在某些狀況下,JVM檢測到一連串的操做都在對同一個對象不斷加鎖,就會將這個鎖加到這一連串操做的外部,好比:

StringBuffer sb=new StringBuffer();
while(i<100){
    sb.append(str);
    i++;
}

上述操做StringBuffer每次添加數據都要加鎖和解鎖,連續100次,這時候JVM就會將鎖加到更外層(while)部分。

(六)逃逸分析

首先問一個常常基礎的虛擬機問題,實例對象存放在虛擬機的哪一個位置?按之前的回答,示例對象放在堆上,引用放在棧上,示例的元數據等存放在方法區或者元空間。

但這是有前提的,前提是示例對象沒有線程逃逸行爲。

JDK1.7開始默認開啓了逃逸分析,所謂逃逸分析,就是指若是一個對象被編譯器發現只能被一個線程訪問,那麼這個對象就不須要考慮同步。JVM就對這種對象進行優化,將堆分配轉化爲棧分配,歸根結底就是虛擬機在編譯過程當中對程序的一種優化行爲。

開啓逃逸分析:­ XX:+DoEscapeAnalysis
關閉逃逸分析: ­XX:­-DoEscapeAnalysis
相關文章
相關標籤/搜索