Synchronized詳細介紹之鎖升級程序
- 前言
- 執行緒與行程的區別
- 行程
- 執行緒
- 區別
- 協程
- JVM執行緒調度原理
- JVM執行緒呼叫程序
- JAVA執行緒與內核執行緒的關系
- 原始碼分析
- 執行緒狀態
- Synchronized鎖
- 加鎖方式
- 原理
- synchronized鎖優化
- monitor監視器
- monitor概念
- Monitor基本元素
- 臨界區的圈定
- Monitor Object
- ObjectMonitor
- monitorenter
- MonitorExit
- Synchronized鎖升級
- 鎖升級原理
- 無鎖升級為偏向鎖
- 偏向鎖升級為輕量級鎖
- 輕量級鎖升級為重量級鎖
- GC標志
- 降級的目的和程序
- 鎖的優缺點
前言
我們在并發編程程序中,會有一些資源或者操作是必須要進行序列化訪問的,就是執行緒之間不能并發的訪問,必須要進行串行訪問,所以就引入了鎖的概念,java中只用的鎖主要有兩種,一種是jdk內置的鎖,一種是juc包下面的鎖,jdk內置的鎖是不要釋放的,由jvm自動給我們釋放鎖,而juc包下面的鎖是有Doug Lea大神開發的,在編程的時候需要在finally進行鎖的釋放,否則很容易導致死鎖;
執行緒與行程的區別
談到并發編程,系統的運行至少要有一個執行緒,而執行緒是運行在行程之中的,行程是存在于作業系統中的,作業系統中可以存在很多行程,這些行程可以并發的進行,而每個行程都必須至少包含一個執行緒,執行緒是運行在行程中的,行程是作業系統資源分配的最小單位,執行緒不直接操作作業系統資源,而是共享行程的資源,所以我們也可以稱執行緒是輕量級的行程,
行程
行程,簡單來說就是我們開發一個程式,存放在服務器的硬碟上,當程式運行時,會在作業系統的記憶體空間形成一塊獨立的記憶體空間,有自己的記憶體地址、堆,上面掛靠的單位是作業系統,作業系統會以行程為單位,分配系統資源,包括CPU時間片段、記憶體空間等,行程是資源分配的最小單位,
執行緒
執行緒是運行在行程中的,執行緒也被稱為輕量級的行程,是作業系統調度(cpu調度)的最小單位,
區別
調度方面:執行緒作為作業系統調度的最小單位,而行程是作為擁有系統資源的基本單位;
并發性: 不僅行程之間可以并發的進行,同一行程之間的不同執行緒也可以并發的執行;
擁有資源:行程是擁有資源的基本單位,執行緒不擁有系統資源,但是執行緒可以訪問隸屬于行程共享資源,
行程維護的是程式所包含的靜態資源如:地址空間、打開的檔案句柄集、問系統狀態、信號處理等;而執行緒所維護的是執行緒資源如:執行緒堆疊資源、調度控制資訊、待處理的資訊等,
系統開銷:當行程進行進行創建或者銷毀時,因為系統都要為行程分配行程資源和回收資源,導致創建行程或者銷毀行程所帶來的開銷明細大于執行緒創建和銷毀的開銷;但是行程有自己獨立的記憶體空間,記憶體地址,一個行程銷毀或者崩潰過后,在保護模式下,其他行程不受影響,而執行緒是運行在行程中的,執行緒是沒有自己單獨的記憶體地址的,一個行程的死掉過后,那么運行這個行程之下的所有執行緒都將被死掉,所以在某種意義上來說,多行程比多執行緒更健壯,但是多行程在行程切換時開銷較大,效率要低一些,
協程
協程,英文Coroutines, 是一種基于執行緒之上,但又比執行緒更加輕量級的存在,協程不是被作業系統內核所管理,而完全是由程式所控制(也就是在用戶態執行),具有對內核來說不可見的特性,這樣帶來的好處就是性能得到了很大的提升,不會像執行緒切換那樣消耗資源

JVM執行緒調度原理
在前面的筆記中曾多次提到我們創建一個執行緒執行,最侄訓提交給內核態去執行,也就是由用戶態切換到內核態,我們想一下,為什么需要執行緒池管理?用戶態創建的所有執行緒都最終都需要內核態去創建執行,那么如果不啟用執行緒池,那么來一個執行緒就由用戶態創建一個執行緒交給內核態去創建執行,那么這樣的開銷是非常大的,就比如本文的主題是synchronized在jdk15之前就是采用內核態來控制的同步操作,在這種模式下,就是通過monitorEnter和monitorExit來控制的,而操作底層是通過mutex互斥信號量來實作的,那么這種情況下有鎖的競爭就會在用戶態和內核態之間反復切換,這樣在性能就影響很大,那么在JDK1.6之后就引入了鎖的優化,也就是鎖升級,由無鎖升級為偏向鎖,如果競爭緊張,由升級輕量級鎖,如果并發上來,升級為重量級鎖,在這個程序中根據不同的業務場景選取不同的鎖來實作;舉個例子,兩個人同時要進一個房間,這個房間是有鑰匙的,鑰匙在前臺管理員處,比如第一個人首先拿到管理員的鑰匙,進入了房間,那么第二人過來也要進這個房間,如果他會去管理員處不斷詢問是否可以進,如果可以,管理員就拿鑰匙給他,但是如果第一個人遲遲沒有出來,那么第二個人會一直不停的去詢問,直到第一個人出來為止,那么如果門上如果有個標記,表示房間有人,直到進入房間的人出來過后標記清除,那么第二個人就可以進入了,那么這樣是不是效率要更高一點,避免了反復去管理員處詢問房間是否可以使用的的性能開銷,所以這里把管理員比喻成作業系統內核,而這兩個人比喻成兩個執行緒,而房間是一個物件,那么物件上不打標記,那么第二個執行緒會反復的去作業系統內核判斷是否可以進入同步代碼塊,那么就會有反復的進行用戶態與內核態直接的切換,這樣的性能開銷是非常大的,而采用物件打標機的模式,那么就只有在用戶態之間切換,這樣就提升了性能,避免了不必要的性能開銷,這也就是synchronized在jdk1.6過后的鎖優化達到的效果,也就是通過鎖升級來實作的,
JVM執行緒呼叫程序

JAVA中創建執行緒由用戶態切換到內核態進行創建執行緒,也就是上面所說執行緒是作業系統調度的最小單位
JAVA執行緒與內核執行緒的關系

原始碼分析
public
class Thread implements Runnable {
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
這個是Thread的原始碼的類資訊,首先實作了Runnable介面,這個介面中run方法是我們執行緒執行的具體業務邏輯方法,如果創建了執行緒,則要實作這個run方法,實作我們自己的業務邏輯,上面已經說了我們的執行緒呼叫是要交給內核去處理的,而java和jvm都是用戶態的模式,所有需要呼叫本地c++方法開啟內核執行緒,所以registerNatives就是注冊本地方法
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
這是執行緒啟動方法,執行緒啟動方法中有一行代碼star0(),這個是呼叫C++中的本地方法啟動執行緒,也就是告訴內核要啟動一個執行緒,而執行緒啟動過后,C++會回呼我們的run方法,start0()這個方法是C++中的方法,如果要分析的話,那么只有使用openjdk的原始碼去分析,目前這個電腦上沒有jdk的原始碼,空了會把jdk執行緒啟動這塊寫出來,我這里總結下啟動流程:
1.java中使用new Thread()創建一個執行緒,然后呼叫start方法啟動一個java級別也就是用戶態的一個執行緒;
2.然后Thread會呼叫本地方法start0(),去呼叫JVM中的JVM_startThread方法進行執行緒創建和啟動;
3.JVM呼叫new javaThread進行執行緒的創建,并且會根據不同的作業系統平臺呼叫對應的作業系統的執行緒創建os::create_thread;
4.創建的執行緒狀態為Initialized,初始化過后呼叫了sync->wait方法進行等待,等到被喚醒過后呼叫thread ->run;
5.呼叫Thread::start方法進行執行緒啟動,此時執行緒狀態設定為RUNNABLE,接著呼叫os::start_thread,根據不同的作業系統選擇不同的執行緒啟動方式;
6.執行緒啟動過后狀態設定為RUNNABLE,并喚醒第四步中處于等待的執行緒,接著執行thread->run方法;
7.JavaThread::run會回呼第一步new Thread中復寫的run方法,
執行緒狀態
//Thread中的執行緒狀態列舉
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
//當一個執行緒被創建的時候就處于NEW的狀態
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
//執行緒呼叫start過后的狀態
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
//執行緒被阻塞的狀態(這個阻塞不是呼叫sleep不是wait,一定要區分開來,官方的解釋是只有當這個執行緒
//進入synchronized 或者再次進入synchronized 才會有這個狀態)
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
//官方解釋一句很明白了,當一個執行緒被呼叫了eait過后就進行了wating,將cpu執行執行時間片給其他執行緒,
//當wait的物件在呼叫notify或者notifyall時會被喚醒,如果呼叫join也會進行waiting
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
//超時等待,官方解釋的意思是當呼叫sleep(time),wait(time),join(time)等等會在指定
的時間內等待
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
//執行緒完成
TERMINATED;
}

Synchronized鎖
加鎖方式

總結:
同步方法鎖:鎖的是當前實體物件
同步靜態方法:鎖的類物件
同步代碼塊:鎖的是指定物件
原理
互斥性:synchronized修飾的方法、代碼塊只能由一個執行緒進行訪問,其他執行緒只能阻塞等著;
可見性:某執行緒 A 對于進入 同步塊之前或在 synchronized 中對于共享變數的操作,對于后續的持有同一個監視器鎖的其他執行緒可見,
在早期的synchronized中的同步鎖實作比較簡單,我們通過實體類分析,看下面的代碼:
public class T0923 {
private int sum = 1;
public static void main(String[] args) {
T0923 t = new T0923();
t.add();
}
public synchronized void add(){
sum ++ ;
}
}
我們查看add方法位元組碼:在命令視窗輸入javap -verbose T0923 .class
public synchronized void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field sum:I
5: iconst_1
6: iadd
7: putfield #2 // Field sum:I
10: return
LineNumberTable:
line 14: 0
line 17: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/t0923/T0923;
}
如果說在方法上加了synchronized的話,那么在falgs上會有一個ACC_SYNCHRONIZED標志,
那么我修改下代碼,synchronized修飾的是代碼塊呢?
public void add(){
synchronized(this){
sum ++ ;
}
}
public void add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field sum:I
9: iconst_1
10: iadd
11: putfield #2 // Field sum:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
上述代碼的計數器
3: monitorenter
15: monitorexit
21: monitorexit
也就是說我們如果鎖的是代碼塊,你那么進入的是monitorenter,退出鎖是monitorexit,但是我們看21行還有一個monitorexit,是因為如果在拋出例外過后,也是進行monitorexit的,所以早期的synchronized就是通過這兩種方式實作的鎖,兩個指令的執行是JVM通過呼叫作業系統的互斥量mutex來實作,被阻塞的執行緒會被掛起、等待重新調度,會導致“用戶態和內核態”兩個態之間來回切換,對性能有較大影響,
其實對于synchronized來說,它會自動釋放鎖,并且在程式拋出例外過后也能自動釋放鎖,從上述代碼的21行就知道,主義看代碼23行,就是在拋出例外之前回把鎖釋放掉,其實從上述的代碼位元組碼序列也可以看出synchronized用的就是monitor機制,常常也被稱作為管程,而monitor機制底層其實用的就是Unsafe的CAS操作,就是誰先進入同步代碼塊,誰就更改標志位當前執行緒,monitor機制是通過作業系統的互斥量mutex來實作的,比如我們來看一段程式:
public class SyncT1 {
private static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
SyncT1 t1 = new SyncT1();
Thread thread1 = new Thread(()->{
t1.test();
},"Thread-1");
Thread thread2 = new Thread(()->{
t1.test();
},"Thread-2");
thread1.start();
thread2.start();
}
private void test(){
synchronized (obj){
System.out.println(Thread.currentThread().getName()+" 進入");
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上述代碼,在sleep時間結束內,執行緒2肯定是進不去的,這個毫無疑問,運行結果如下:

執行緒1進入了同步代碼塊,然后sleep過后,執行緒2是無法進入同步代碼塊的,所以我們修改下代碼:
private void test(){
synchronized (obj){
System.out.println(Thread.currentThread().getName()+" 進入");
try {
UnSafeFactory.getUnsafe().monitorExit(obj);
Thread.sleep(1000000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
UnSafeFactory.getUnsafe().monitorExit(obj)讓obj這個物件的監視器退出,執行結果如下:

可以看到執行緒2已經進入了,因為直行了monitorExit過后,synchronized已經退出了,所以通過這里演示可以知道synchronized鎖定代碼塊是通過物件監視器來實作的,
synchronized鎖優化
從JDK6開始,就對synchronized的實作機制進行了較大調整,包括使用JDK5引進的CAS自旋之外,還增加了自適應的CAS自旋、鎖消除、鎖粗化、偏向鎖、輕量級鎖這些優化策略,在 JDK 1.6 中默認是開啟偏向鎖的,可以通過-XX:-UseBiasedLocking來禁用偏向鎖,使用-XX:-UseSpinning引數關閉自旋鎖優化;-XX:PreBlockSpin引數修改默認的自旋次數,
偏向鎖:無實際競爭,且將來只有第一個申請鎖的執行緒會使用鎖,
輕量級鎖:無實際競爭,多個執行緒交替使用鎖;允許短時間的鎖競爭,
重量級鎖:有實際競爭,且鎖競爭時間長,
monitor監視器
monitor概念
管程,監視器,在作業系統中,存在著semaphore和mutex,即信號量和互斥量,使用基本的mutex進行開發時,需要小心的使用mutex的down和up操作,否則容易引發死鎖問題,為了更好的撰寫并發程式,在mutex和semaphore基礎上,提出了更高層次的同步原語,實際上,monitor屬于編程語言的范疇,C語言不支持monitor,而java支持monitor機制,
一個重要特點是,在同一時間,只有一個執行緒/行程能進入monitor所定義的臨界區,這使得monitor能夠實作互斥的效果,無法進入monitor的臨界區的行程/執行緒,應該被阻塞,并且在適當的時候被喚醒,顯然,monitor作為一個同步工具,也應該提供這樣管理執行緒/行程的機制,
Monitor基本元素
1.臨界區
2.monitor物件和鎖
3.條件變數以及定義在monitor物件上的wait,signal操作,
使用monitor主要是為了互斥進入臨界區,為了能夠阻塞無法進入臨界區的行程,執行緒,需要一個monitor object來協助,這個object內部會有相應的資料結構,例如串列,用來保存被阻塞的執行緒;同時由于monitor機制本質是基于mutex原語的,所以object必須維護一個基于mutex的鎖,
臨界區的圈定
被synchronized關鍵字修飾的方法,代碼塊,就是monitor機制的臨界區
Monitor Object
我們知道java的所有物件都都默認繼承了java.lang.Object,一個Object物件在記憶體中的默認都是16byte,物件在記憶體中布局是mark word,型別指標,實體資料和對齊填充,如下圖:

根據筆記上面的描述來說,在JDK1.6過后,對鎖做了優化,比如我們上面所描述的例子,進入房間的例子,那么就會頻繁的與內核進行互動,這樣就會出現用戶態與內核態頻繁的互動導致性能低下,因為synchronized鎖的物件,而jdk的鎖優化就會在物件的mark word中進行打標記,這樣就不用每次都去內核態通過互斥量mutex來進行同步控制;我們知道java.lang.Object中的wait和notify以及notifyAll是必須要在synchronized中進行使用的,也就是說當執行緒進入了synchronized代碼塊過后,可以通過wait、notify/notifyall進行等待喚醒操作,這個是在同步代碼中使用的,如果想通過執行緒通信來阻塞和喚醒,那么可以使用juc包下面的相關執行緒類,我們知道了Object中的wait和notify等操作過后,那么在C++中JVM是通過ObjectMonitor這個類來實作的monitor機制,而monitor機制其實也是用的CAS機制,待會兒分析下原始碼 ,這里先來看下monitor Object機制

The Owner表示當前執行緒執行的同步代碼塊
Entry Set表示等待進入執行緒的佇列
Wait Set表示進入了同步代碼塊而由于sleep或者wait進入的佇列
上圖的表達的就是一個Monitor Object機制的原理
簡單來說就是所有執行緒都想要進入同步代碼塊,首先執行緒3先進入同步代碼塊,然后由于執行緒3呼叫了wait或者sleep進入了Wait Set,這個時候執行緒3讓出了CPU的時間片段,其他執行緒就開始競爭,下個執行緒4得到也競爭得到了進入同步代碼塊的權限,然后也執行緒4和3一樣呼叫了wait或者sleep進入了等待對壘,這個時候又進入競爭,執行緒5進入了執行同步代碼塊,然后執行緒5執行完成了釋放了鎖,這個時候1,2,3,4開始競爭,而執行緒4得到了權限從wait set進入同步代碼塊執行,其他執行緒進入等待佇列,上圖要表達的意思就是這樣的,至少在我看來是這樣的;
總結來說,就是ObjectMonitor維護了兩個佇列,一個進入的等待佇列,一個是wait的等待佇列,wait的等待佇列是由于在同步代碼中呼叫了wait或者sleep進入的,而Owner是只允許一個執行緒進入執行的,當Owner執行完成過后,兩個佇列中的執行緒開始進入競爭,因為synchronized是非公平的,所以競爭中,誰都有可能競爭到,如果某個執行緒優先級比較高,那么就算進入了等待佇列,那么很有可能每次都有機會論到它執行,
ObjectMonitor
由于我對C++不是太懂,只能看懂一些,小部分,所以我也只能把實作的意思大概讀懂
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;//表示獲得鎖的次數,因為synchronized是可重入鎖,所以可以獲得多次
_object = NULL;
_owner = NULL;//當前執行的執行緒
_WaitSet = NULL;//呼叫了wait或者sleep進入的佇列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//在進入同步代碼塊之前的阻塞佇列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
monitorenter
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
//CAS操作,self是當前想要進入的先,owner監視器中的執行緒物件,
//而NULL就是代表我們要比較的值
//如果owner==null,比較成功,則證明是可以進入的,那么這個時候將owner設定為self,也就是當前進入的執行緒
//如果cas成功,則回傳比較前的值null
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
//代表cas成功了,獲得鎖
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
//如果cur不等于null,cur == self,那么證明之前獲得鎖的執行緒就是它自己,所以這里將獲得鎖的次數+1,因為synchronized是可重入鎖
_recursions ++ ;
return ;
}
MonitorExit
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * Self = THREAD ;
//判斷當前線程是否是正在執行的執行緒,如果不是,把owner設定為當前執行緒,并且鎖
//次數為0,如果鎖次數為0,則為釋放鎖
if (THREAD != _owner) {
if (THREAD->is_lock_owned((address) _owner)) {
// Transmute _owner from a BasicLock pointer to a Thread address.
// We don't need to hold _mutex for this transition.
// Non-null to Non-null is safe as long as all readers can
// tolerate either flavor.
assert (_recursions == 0, "invariant") ;
_owner = THREAD ;
_recursions = 0 ;
OwnerIsThread = 1 ;
} else {
// NOTE: we need to handle unbalanced monitor enter/exit
// in native code by throwing an exception.
// TODO: Throw an IllegalMonitorStateException ?
TEVENT (Exit - Throw IMSX) ;
assert(false, "Non-balanced monitor enter/exit!");
if (false) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
return;
}
}
// 如果_recursions次數不為0.自減
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}
Synchronized鎖升級
鎖升級原理
我們通過前面的筆記已經知道synchronized鎖的是物件,而鎖優化過后,默認是無鎖,再到偏向鎖,輕量級鎖,重量級鎖,那么這是一個程序,而且這個程序是用戶態進行的,沒有到內核態進行,那么synchronized是如何實作的,因為鎖的是物件,那么就是在物件的物件頭中實作的,關于物件在記憶體中的布局在jvm的筆記中已經詳細記錄,我這里就簡單來說,synchronized是通過物件頭的markword來進行,那么markword到底是什么樣的,我們以64位機器來看:

圖(1)

圖(2)
通過圖1和圖2可以看到:
lock狀態(2bit):
1.01是無鎖或者偏向鎖;
2.00是輕量級鎖;
3.10是重量級鎖;
4.11是GC標記,表示可以被GC了,被GC打了標記了,
biased_lock(1bit):是否偏向鎖的標志,0=否 ,1=是
age(4bit):物件的分代年齡,占4bit,所以分代年齡的最大年齡是15,設定智能是小于等于15;
unused:表示未使用的
epoch(2bit):表示偏向鎖的時間戳
identity_hascode(31bit):物件的hashcode值
ptr_to_lock_record(62bit):輕量級鎖狀態下,指向物件監視器的monitor的指標
prt_to_heavyweight_monitor:重量級鎖狀態下,指向物件監視器的monitor指標
總結:



無鎖升級為偏向鎖
在JDK1.6過后,默認是開啟了偏向鎖的,偏向鎖的性能較低,偏向鎖適用于單執行緒的環境下,所以要根據具體的業務情況來使用,如果synchronized在第一個執行緒進入的情況下,默認修改為偏向鎖,將當前執行緒的ID更新到物件頭的markword的執行緒ID中,增加偏向鎖的時間戳以及偏向鎖的標志修改為1

比如有幾個執行緒同時訪問同步代碼塊,那么只有一個執行緒可以進入,那么初始的object markword肯定是無鎖的,也就是上圖的無鎖物件頭,那么這個時候執行緒1把當前執行緒的ID通過CAS修改到markword中,這個程序中的CAS肯定是能成功的,不成功就不是無鎖升級為偏向鎖了,還是其他鎖升級的程序了;這個時候其他執行緒是在執行緒1未退出同步代碼塊的時候是沒有八法進入同步代碼塊,也就是沒有辦法獲取鎖,那么其他獲取cpu執行權限的執行緒會通過CAS修改執行緒ID為當前執行緒,但是如果執行緒1沒有退出同步代碼塊,而后續執行緒通過CAS進行修改是不能成功的,那么這個時候后續的執行緒就將synchronized升級為輕量級鎖,也就是下一個鎖升級程序,
偏向鎖升級為輕量級鎖
偏向鎖升級為輕量級鎖是在執行緒有競爭的情況下,執行緒1遲遲沒有退出同步代碼塊,而執行緒2又要競爭這把鎖,而執行緒2通過CAS自適應自旋一直沒有成功,這個時候它就升級為輕量級鎖,如果在CAS的程序中,執行緒1退出了同步代碼塊,那么這個時候執行緒2CAS成功,是不會升級為輕量級鎖,所以偏向鎖適用于單執行緒的環境下

輕量級鎖是在執行緒有一定的競爭的時候想要進入同步代碼塊,而如果這個時候之前運行在synchronized的執行緒退出了,那么不會升級為輕量級鎖,還是偏向鎖,如果執行緒2cas結束過后,執行緒1還沒有退出就會進行鎖升級,偏向鎖升級為輕量級鎖,這個升級程序非常消耗性能,所以有很多公司都是禁止出現偏向鎖的,因為偏向鎖升級為輕量級鎖的時候,是需要撤銷偏向鎖的,撤銷偏向鎖的程序如下:
1.在一個安全點停止所有擁有鎖的執行緒;
2.遍歷執行緒堆疊,如果存在鎖記錄,需要修復鎖記錄和MarkWord,使其變成無鎖的狀態;
3.喚醒當前執行緒,將當前鎖升級為輕量級鎖;
所以這個程序是非常消耗性能的,所以不適合在多執行緒的環境下使用偏向鎖
輕量級鎖升級為重量級鎖

重量級鎖在并發非常高的情況下啟用,就是鎖的競爭非常激勵,比如執行緒1首先將在執行緒堆疊上開辟一定的空間來存盤mark word,并且相互指向,然后開始執行同步代碼,而這個時候很多執行緒都過來了,那么這些執行緒也要拷貝markword到執行緒堆疊中,然后cas修改lock record與mark word的相互指向,這個時候只有一個執行緒能夠成功,其他執行緒都需要cas,如果執行緒1沒有同步代碼塊沒有指向完成,其他執行緒是沒有辦法自旋成功,那么就就那些鎖膨脹,升級為重量級鎖,重量級鎖升級過后執行緒的阻塞是由內核進行處理的,所以性能較低,
GC標志
我們知道GC在每次進行的時候其實就是對物件的操作,物件的物件頭中的markword進行操作,如果這個物件可以被GC了,那么GC會在在markword的鎖狀態設定為11,表示新一輪的gc開始了,而物件的age是最大15次,每次gc,age+1,如果達到了15次還存活就移植到老年代;所以這里有個問題就是如果我們的object物件升級為重量級鎖了,那么是不是一直是重量級鎖呢?我們知道鎖的升級是不能降級的,也就是說輕量級鎖不能降級為偏向鎖,偏向鎖不能降級為無鎖,那如果說我們的鎖升級為重量級鎖了,過了很久都沒有執行緒來訪問,下一次執行緒來訪問的時候還是重量級鎖嗎?不是的,JVM沒有這么的傻,也就是說在很久沒有執行緒訪問的情況下會進行降級,但是降級是直接降級為無鎖狀態,
降級的目的和程序
因為基本物件鎖的實作優先于重量級鎖的使用,JVM會嘗試在SWT的停頓中堆處于空閑狀態重量級鎖進行降級操作,這個降級程序是如何實作的呢?我們知道在STW時,所有的JAVA執行緒都將暫停在安全點SafePoint,此時VMThread通過對所有Monitor的遍歷,或者通過對所有依賴于MonitorInUseLists值得當前正在使用中的Monitor子序列進行遍歷從而得到哪些是未被使用的Monitor作為降級物件,
可以降級的Monitor物件
重量級鎖的降級程序發生在STW階段,降級物件就是哪些僅僅能被VMThread訪問而沒有被其他JavaThread訪問的Monitor物件,
鎖的優缺點

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/131755.html
標籤:AI
上一篇:全域添加日志-自定義注解
