在併發編程中存在線程安全問題,主要緣由有:1.存在共享數據 2.多線程共同操做共享數據。關鍵字synchronized能夠保證在同一時刻,只有一個線程能夠執行某個方法或某個代碼塊,同時synchronized能夠保證一個線程的變化可見(可見性),便可以代替volatile。java
synchronized能夠保證方法或者代碼塊在運行時,同一時刻只有一個方法能夠進入到臨界區,同時它還能夠保證共享變量的內存可見性,它能夠:git
Java中每個對象均可以做爲鎖,這是synchronized實現同步的基礎:github
在同步的時候是獲取對象的monitor,即獲取到對象的鎖。那麼對象的鎖怎麼理解?無非就是相似對對象的一個標誌,那麼這個標誌就是存放在Java對象的對象頭。Java對象頭裏的Mark Word裏默認的存放的對象的Hashcode,分代年齡和鎖標記位。編程
每一個對象分爲三塊區域:對象頭、實例數據和對齊填充數組
鎖狀態 | 25bit | 4bit | 1bit是不是偏向鎖 | 2bit鎖標記位 |
---|---|---|---|---|
無鎖 | 對象的haahcode | 分代年齡 | 0 | 01 |
輕量級鎖 | 指向棧中鎖記錄的指針 | 合併第一列 | 合併第一列 | 00 |
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 合併第一列 | 合併第一列 | 10 |
GC標誌 | 空 | 合併第一列 | 合併第一列 | 11 |
偏向鎖 | 線程ID(23bit)和Epoch(2bit) | 對象分代年齡 | 1 | 01 |
如上表在Mark Word會默認存放hasdcode,年齡值以及鎖標誌位等信息安全
鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率。bash
從語法上講,Synchronized能夠把任何一個非null對象做爲"鎖",在HotSpot JVM實現中,鎖有個專門的名字:對象監視器(Object Monitor)。能夠把它理解爲 一個同步工具,也能夠描述爲 一種同步機制,實現了在一個時間點,最多隻有一個線程在執行管程的某個子程序,這個機制的保障來源於監視鎖Monitor,每一個對象都擁有本身的監視鎖Monitor。數據結構
咱們能夠把監視器理解爲一個醫院,醫院裏面只要一個醫生,每次只能看一個病人(線程),若是一個病人想看病,他首先要在走廊裏面排隊(Entry Set),依次進入看病,可是假如某個正在看病的人可能暈血或者血糖低不能暫時繼續看病(線程被掛起),這時候不能強行給他看,也不能讓後面的病人等他一個,因而就要送他到休息室去休息(Wait Set),休息室裏面呆的都是由於各類緣由不能繼續看病的病人,等休息好了,還能夠繼續去看病。以下圖多線程
總之,監視器是一個用來監視這些線程進入特殊的房間的。他的義務是保證(同一時間)只有一個線程能夠訪問被保護的數據和代碼。在Java虛擬機(HotSpot)中,Monitor是基於C++實現的ObjectMonitor,其主要數據結構以下併發
ObjectMonitor() {
_header = NULL;
_count = 0; //用來記錄該線程獲取鎖的次數
_waiters = 0,
_recursions = 0; //鎖的重入次數
_object = NULL;
_owner = NULL; //指向當前持有ObjectMonitor對象的線程
_WaitSet = NULL; //存放wait狀態的線程隊列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //這是一個和_WaitSet相似存等待線程的地方,
//可是是否存在這裏是要根據Policy的值(這裏不知道說的對不對,順便說下,這玩意兒每次看都覺得是cxk)
FreeNext = NULL ;
_EntryList = NULL ; //存放處於等待鎖的線程隊列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
複製代碼
當多個線程同時訪問一段同步代碼時,首先進入 _EntryList
,當某個線程獲取到對象的monitor
後進入_Owner
區域並把monitor
中的_owner
變量設置爲當前線程,同時_count
加一,得到對象鎖。 若是持有monitor的線程被掛起(例如調用wait方法),將釋放當前持有的monitor,_owner
變量回復爲null,_count
減一,同時該線程進入_WaitSet
隊列中等待被喚醒(notify),若是當前線程順利執行完代碼塊後會釋放monitor
並復位變量的值,以便下一個線程進來獲取monitor
鎖,下面看個例子。
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
System.out.printf("synchronized");
}
function();
}
private static void function() {
System.out.printf("function");
}
}
上面的代碼中有一個同步代碼塊,鎖住的是類對象,而且還有一個同步靜態方法,鎖住的依然是該類的類對象。下面是字節碼文件
public class com.example.javalib.SynchronizedDemo {
public com.example.javalib.SynchronizedDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // class com/example/javalib/SynchronizedDemo
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: invokestatic #3 // Method function:()V
18: return
複製代碼
上面的4,6,12行就是須要注意的部分了,這是添加Synchronized關鍵字以後纔會出現的。執行同步代碼塊首先要執行monitorenter
,退出的時候執行monitorexit
指令。 使用Synchronized之因此可以進行同步,其關鍵就是對對象的監視器monitor
的獲取,當執行線程獲取到monitor後才能繼續執行下去,不然只能繼續等待。 上面的demo中同步代碼塊後還有一個靜態方法,這個方法是同步的,並且該方法鎖的對象依然是這個類對象,那麼執行線程就沒必要再去獲取這個鎖,從字節碼中能夠看到,有一條monitorenter
指令和兩條monitorexit
指令,並無第二次獲取鎖的指令,這就是鎖的重入性:即在同一個鎖程中,線程不須要去再次獲取同一把鎖,Synchronized先天具備重入性。每一個對象擁有一個計數器,當線程獲取該對象鎖後,計數器就會加一,釋放鎖後就會將計數器減一。 任意一個對象都擁有本身的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取該對象的監視器才能進入同步塊和同步方法,若是沒有獲取到監視器的線程將會被阻塞在同步塊和同步方法的入口處,進入到BLOCKED狀態,關於線程的狀態能夠看這篇文章
從上面咱們知道了sychronized
加鎖的時候,會調用objectMonitor
的enter
方法,解鎖的時候會調用exit方法。事實上,只有在JDK1.6以前,synchronized
的實現纔會直接調用ObjectMonitor
的enter
和exit
,這種鎖被稱之爲重量級鎖。爲何說這種方式操做鎖很重呢? 由於Java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統的幫忙,這就要從用戶態轉換到核心態,所以狀態轉換須要花費不少的處理器時間,對於代碼簡單的同步塊(如被synchronized修飾的get 或set方法)狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長,因此說synchronized是java語言中一個重量級的操縱。 因此,在JDK1.6中出現對鎖進行了不少的優化,進而出現輕量級鎖,偏向鎖,鎖消除,適應性自旋鎖,鎖粗化(自旋鎖在1.4就有 只不過默認的是關閉的,jdk1.6是默認開啓的),這些操做都是爲了在線程之間更高效的共享數據 ,解決競爭問題。
以上文章是解決一個同步問題時發現synchronized知識點只知其一;不知其二後查找資料後摘抄的筆記,算是本身我的的整理,漏了什麼歡迎指出來。