由淺深入理解java多線程,java并發(fā),synchronized實現(xiàn)原理及線程鎖機制
[TOC]
多進程是指操作系統(tǒng)能同時運行多個任務(程序)。
多線程是指在同一程序中有多個順序流在執(zhí)行。
一,線程的生命周期
[圖片上傳失敗...(image-370bc0-1635087555179)]
-
新建狀態(tài):
使用 new 關鍵字和 Thread 類或其子類建立一個線程對象后,該線程對象就處于新建狀態(tài)。它保持這個狀態(tài)直到程序 start() 這個線程。
-
就緒狀態(tài):
當線程對象調用了start()方法之后,該線程就進入就緒狀態(tài)。就緒狀態(tài)的線程處于就緒隊列中,要等待JVM里線程調度器的調度。
-
運行狀態(tài):
如果就緒狀態(tài)的線程獲取 CPU 資源,就可以執(zhí)行 run(),此時線程便處于運行狀態(tài)。處于運行狀態(tài)的線程最為復雜,它可以變?yōu)樽枞麪顟B(tài)、就緒狀態(tài)和死亡狀態(tài)。
-
阻塞狀態(tài):
如果一個線程執(zhí)行了sleep(睡眠)、suspend(掛起)等方法,失去所占用資源之后,該線程就從運行狀態(tài)進入阻塞狀態(tài)。在睡眠時間已到或獲得設備資源后可以重新進入就緒狀態(tài)??梢苑譃槿N:
- 等待阻塞:運行狀態(tài)中的線程執(zhí)行 wait() 方法,使線程進入到等待阻塞狀態(tài)。
- 同步阻塞:線程在獲取 synchronized 同步鎖失敗(因為同步鎖被其他線程占用)。
- 其他阻塞:通過調用線程的 sleep() 或 join() 發(fā)出了 I/O 請求時,線程就會進入到阻塞狀態(tài)。當sleep() 狀態(tài)超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態(tài)。
-
死亡狀態(tài):
一個運行狀態(tài)的線程完成任務或者其他終止條件發(fā)生時,該線程就切換到終止狀態(tài)。
二,線程的調度
調整線程優(yōu)先級
Java線程有優(yōu)先級,優(yōu)先級高的線程會獲得較多的運行機會。
線程睡眠
Thread.sleep(long millis)方法,使線程轉到阻塞狀態(tài)。millis參數(shù)設定睡眠的時間,以毫秒為單 位。當睡眠結束后,就轉為就緒(Runnable)狀態(tài)。sleep()平臺移植性好。
線程等待
Object類中的wait()方法,導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價于調用 wait(0) 一樣。 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價于調用 wait(0) 一樣。
線程讓步
Thread.yield() 方法,暫停當前正在執(zhí)行的線程對象,把執(zhí)行機會讓給相同或者更高優(yōu)先級的線 程。
線程加入
join()方法,等待其他線程終止。在當前線程中調用另一個線程的join()方法,則當前線程轉入阻 塞狀態(tài),直到另一個進程運行結束,當前線程再由阻塞轉為就緒狀態(tài)。
線程喚醒
Object類中的notify()方法,喚醒在此對象監(jiān)視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,并在對實現(xiàn)做出決定時發(fā)生。線程通過調用其中一個 wait 方法,在對象的監(jiān)視器上等待。 直到當前的線程放棄此對象上的鎖定,才能繼續(xù)執(zhí)行被喚醒的線程。被喚醒的線程將以常規(guī)方式與在該對象上主動同步的其他所有線程進行競爭;例如,喚醒的線程在作為鎖定此對象 的下一個線程方面沒有可靠的特權或劣勢。類似的方法還有一個notifyAll(),喚醒在此對象監(jiān)視器上等待的所有線程。
三,創(chuàng)建多線程的方式
1,通過實現(xiàn)Runnable接口
//
public class T3 implements Runnable {
String a;
//構造方法
public T3(String a) {
this.a = a;
}
public void run() {
System.out.println(a);
}
}
//開啟了兩個線程,實例化了兩個對象,但是現(xiàn)在還沒有做數(shù)據(jù)共享的驗證
public static void main(String[] args) {
new Thread(new T3("上海")).start();
new Thread(new T3("北京")).start();
}
使用接口,在啟動的多線程的時候,需要先通過 Thread 類的構造方法 Thread(Runnable target) 構造出對象,然后調用 Thread 對象的 start() 方法來運行多線程代碼。
輸出結果:
結果1 結果2
北京 上海
上海 北京
2,通過繼承Thread類
//
public class T1 extends Thread {
String a;
//構造方法
public T1(String a) {
this.a = a;
}
public void run() {
System.out.println(a);
}
}
//開啟了兩個線程,實例化了兩個對象,但是現(xiàn)在還沒有做數(shù)據(jù)共享的驗證
public class T2 {
public static void main(String[] args) {
new T1("上海").start();
new T1("北京").start();
}
}
輸出結果:
結果1 結果2
北京 上海
上海 北京
四,多線程間的數(shù)據(jù)共享
1,Runnable接口實現(xiàn)多線程的數(shù)據(jù)共享
//寫法1
public class T3 implements Runnable {
int b = 10;
String a;
public T3(String a) {
this.a = a;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(a + b--);
}
}
}
//寫法2
public class T3 implements Runnable {
int b = 10;
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + b--);
}
}
}
//開啟了兩個線程,實例化了1個對象
public class T4 {
public static void main(String[] args) {
T3 t3 = new T3();
new Thread(t3, "上海").start();
new Thread(t3, "北京").start();
}
}
兩個線程,一起操作同一個數(shù)值,每個線程各操作5次,未出現(xiàn)重復的數(shù)值,實現(xiàn)數(shù)據(jù)共享。部分輸出結果為:
| 輸出結果1 | 輸出結果2 | 輸出結果3 | 輸出結果4 | 輸出結果5 |
|---|---|---|---|---|
| 上海10 | 上海10 | 上海10 | 上海10 | 北京9 |
| 上海9 | 上海9 | 北京9 | 北京9 | 北京8 |
| 上海8 | 上海8 | 北京7 | 北京7 | 北京7 |
| 上海6 | 上海7 | 北京6 | 北京6 | 北京6 |
| 上海5 | 上海6 | 北京5 | 上海8 | 北京5 |
| 北京7 | 北京5 | 北京4 | 上海4 | 上海10 |
| 北京4 | 北京4 | 上海8 | 上海3 | 上海4 |
| 北京3 | 北京3 | 上海3 | 北京5 | 上海3 |
| 北京2 | 北京2 | 上海2 | 上海2 | 上海2 |
| 北京1 | 北京1 | 上海1 | 北京1 | 上海1 |
2,Thread類實現(xiàn)多線程的數(shù)據(jù)共享
不方便做到
//
public class T1 extends Thread {
int b = 10;
String a;
public T1(String a) {
this.a = a;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(a + b--);
}
}
}
//開啟了兩個線程,實例化了2個對象
public class T2 {
public static void main(String[] args) {
new T1("上海").start();
new T1("北京").start();
}
}
輸出結果
兩個線程,一起操作同一個數(shù)值,每個線程各操作5次,出現(xiàn)了重復的數(shù)值,未實現(xiàn)數(shù)據(jù)共享。部分輸出結果為:
| 輸出結果1 | 輸出結果2 | 輸出結果3 | 輸出結果4 | 輸出結果5 |
|---|---|---|---|---|
| 上海10 | 北京10 | 上海10 | 上海10 | 上海10 |
| 上海9 | 北京9 | 北京10 | 上海9 | 北京10 |
| 上海8 | 北京8 | 上海9 | 上海8 | 北京9 |
| 上海7 | 北京7 | 上海8 | 北京10 | 上海9 |
| 上海6 | 北京6 | 北京9 | 北京9 | 北京8 |
| 北京10 | 上海10 | 北京8 | 北京8 | 上海8 |
| 北京9 | 上海9 | 北京7 | 北京7 | 北京7 |
| 北京8 | 上海8 | 北京6 | 北京6 | 上海7 |
| 北京7 | 上海7 | 上海7 | 上海7 | 北京6 |
| 北京6 | 上海6 | 上海6 | 上海6 | 上海6 |
總結
實現(xiàn) Runnable 接口比繼承 Thread 類所具有的優(yōu)勢:
1):適合多個相同的程序代碼的線程去處理同一個資源
2):可以避免 java 中的單繼承的限制
五,synchronized實現(xiàn)多線程數(shù)據(jù)共享
當存在多個線程操作共享數(shù)據(jù)時,需要保證同一時刻有且只有一個線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進行。
當兩個并發(fā)線程訪問同一個對象中的 synchronized 代碼塊時,在同一時刻只能有一個線程得到執(zhí)行,另一個線程受阻塞,必須等待當前線程執(zhí)行完這個代碼塊以后才能執(zhí)行該代碼塊。此時線程是互斥的,因為在執(zhí)行代碼塊時會鎖定當前的對象,只有執(zhí)行完該代碼塊才能釋放該對象鎖,下一個線程才能執(zhí)行并鎖定該對象。
1,修飾實例方法
通過Runnable接口
//
public class T3 implements Runnable {
int b = 10;
public synchronized int aaa() {
return b--;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + aaa());
}
}
}
//開啟了兩個線程,實例化了1個對象
public class T4 {
public static void main(String[] args) {
T3 t3 = new T3();
new Thread(t3, "上海").start();
new Thread(t3, "北京").start();
}
}
輸出結果;參照線程間數(shù)據(jù)共享的Runnable接口的輸出結果??蓪崿F(xiàn)數(shù)據(jù)共享
Thread類
public class T1 extends Thread {
int b = 10;
String a;
public T1(String a) {
this.a = a;
}
public synchronized int aaa() {
return b--;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(a + aaa());
}
}
}
//開啟了兩個線程,實例化了2個對象
public class T2 {
public static void main(String[] args) {
new T1("上海").start();
new T1("北京").start();
}
}
輸出結果;參照線程間數(shù)據(jù)共享的Thread類的輸出結果。沒有實現(xiàn)數(shù)據(jù)共享
如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當前對象鎖是obj2),這樣是允許的,因為兩個實例對象鎖并不同相同, 此時如果兩個線程操作數(shù)據(jù)并非共享的。
雖然我們使用synchronized修飾了 aaa 方法,但卻new了兩個不同的實例對象,這也就意味著存在著兩個不同的實例對象鎖,因此t1和t2都會進入各自的對象鎖,也就是說t1和t2線程使用的是不同的鎖,因此線程安全是無法保證的。
2,修飾靜態(tài)方法
通過Runnable接口
//
public class T3 implements Runnable {
static int b = 10;
public static synchronized int aaa() {
return b--;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + aaa());
}
}
}
//開啟了兩個線程,實例化了1個對象
public class T4 {
public static void main(String[] args) {
T3 t3 = new T3();
new Thread(t3, "上海").start();
new Thread(t3, "北京").start();
}
}
輸出結果;參照線程間數(shù)據(jù)共享的Runnable接口的輸出結果。可實現(xiàn)數(shù)據(jù)共享
Thread類
public class T1 extends Thread {
static int b = 10;
String a;
public T1(String a) {
this.a = a;
}
public static synchronized int aaa() {
return b--;
}
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(a + aaa());
}
}
}
//開啟了兩個線程,實例化了2個對象
public class T2 {
public static void main(String[] args) {
new T1("上海").start();
new T1("北京").start();
}
}
輸出結果;參照線程間數(shù)據(jù)共享的Runnable接口的輸出結果??蓪崿F(xiàn)數(shù)據(jù)共享
synchronized作用于靜態(tài)的 aaa 方法,這樣的話,對象鎖就當前類對象,由于無論創(chuàng)建多少個實例對象,但對于的類對象擁有只有一個,所有在這樣的情況下對象鎖就是唯一的。
3,修飾同步代碼塊
能縮小代碼段的范圍就盡量縮小,能在代碼段上加同步就不要再整個方法上加同步。這叫減小鎖的粒度,使代碼更大程度的并發(fā)。鎖的代碼段太長了,別的線程就要等很久,等的花兒都謝了。
通過Runnable接口
//
public class T3 implements Runnable {
int b = 10;
public void run()
synchronized (this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + aaa());
}
}
}
}
public class T4 {
public static void main(String[] args) {
T3 t3 = new T3();
new Thread(t3, "上海").start();
new Thread(t3, "北京").start();
}
}
4,總結
start()方法的調用后并不是立即執(zhí)行多線程代碼,而是使得該線程變?yōu)榭蛇\行態(tài)(Runnable),什么時候運行是由操作系統(tǒng)決定的。
請記住,上下文的切換開銷也很重要,如果你創(chuàng)建了太多的線程,CPU 花費在上下文的切換的時間將多于執(zhí)行程序的時間!
- 沒有 synchronized 關鍵字的默認情況。如線程間數(shù)據(jù)共享一節(jié)中并沒有用該關鍵字。
- 實例化多個對象,也就存在多個對象鎖,每個線程用不同的對象鎖,數(shù)據(jù)自然無法共享。
- 不管實例化多少個對象,如果synchronized作用于靜態(tài)方法,由于靜態(tài)的特殊性,該對象只會有一個,那么在這樣的情況下對象鎖又是唯一的。
六,synchronized實現(xiàn)原理
1,synchronized修飾后的字節(jié)碼
上述synchronized主要是了解數(shù)據(jù)共享的,其字節(jié)碼并不直觀看鎖相關的,另外寫了個如下所示;
public class T5 {
//修飾方法
public synchronized void aaa(){
}
//修飾靜態(tài)方法
public static synchronized void bbb(){
}
//修飾類
public void ccc(){
synchronized (T5.class){
}
}
//修飾this
public void ddd(){
synchronized (this){
}
}
}
window下取其字節(jié)碼內容

javac T5.java 編譯生成class文件
javap -v -p -s -sysinfo -constants T5.class ,使用javap 工具查看生成的class文件
Classfile /D:/Test/Java/src/com/lgx/test/T5.class
Last modified 2021-10-20; size 549 bytes
MD5 checksum f3500e41224be759d110519587593b09
Compiled from "T5.java"
public class com.lgx.test.T5
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#18 // java/lang/Object."<init>":()V
#2 = Class #19 // com/lgx/test/T5
#3 = Class #20 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 aaa
#9 = Utf8 bbb
#10 = Utf8 ccc
#11 = Utf8 StackMapTable
#12 = Class #19 // com/lgx/test/T5
#13 = Class #20 // java/lang/Object
#14 = Class #21 // java/lang/Throwable
#15 = Utf8 ddd
#16 = Utf8 SourceFile
#17 = Utf8 T5.java
#18 = NameAndType #4:#5 // "<init>":()V
#19 = Utf8 com/lgx/test/T5
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/Throwable
{
public com.lgx.test.T5();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public synchronized void aaa();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 6: 0
public static synchronized void bbb();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 9: 0
public void ccc();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/lgx/test/T5
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: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
LineNumberTable:
line 12: 0
line 13: 5
line 14: 15
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 10
locals = [ class com/lgx/test/T5, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public void ddd();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
LineNumberTable:
line 17: 0
line 18: 4
line 19: 14
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ class com/lgx/test/T5, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "T5.java"
由字節(jié)碼可知,當修飾方法時,JVM采用 ACC_SYNCHRONIZED 標記符來實現(xiàn)同步。 當修飾類時,JVM采用monitorenter、monitorexit兩個指令來實現(xiàn)同步。(在字節(jié)碼里面可以看見在修飾類時,有Exception table,這是因為,JVM會自動在synchronized代碼塊中加入異常捕獲,從而保證代碼拋出異常時,仍能夠釋放當前線程占用的鎖,避免出現(xiàn)死鎖現(xiàn)象。)
在synchronized修飾方法時是添加ACC_SYNCHRONIZED標識。方法級同步是隱式執(zhí)行的,作為方法調用和返回的一部分。 同步方法在運行時常量池的 method_info 結構中通過 ACC_SYNCHRONIZED 標志進行區(qū)分,該標志由方法調用指令檢查。 當調用設置了 ACC_SYNCHRONIZED 的方法時,執(zhí)行線程進入監(jiān)視器(monitor),調用方法本身,并退出monitor,無論方法調用是正常完成還是突然完成。 在執(zhí)行線程擁有monitor期間,沒有其他線程可以進入它。 如果在調用同步方法過程中拋出異常并且同步方法沒有處理該異常,則在異常重新拋出同步方法之前,該方法的monitor會自動退出。
在synchronized修飾類時是通過monitorenter、monitorexit指令。 當且僅當monitor有所有者時,monitor才被鎖定。 執(zhí)行monitorenter 的線程嘗試獲得與objectref 關聯(lián)的monitor的所有權,如下所示:
- 如果與objectref 關聯(lián)的monitor的條目計數(shù)為零,則該線程進入monitor并將其條目計數(shù)設置為1,然后該線程是monitor的所有者。
- 如果線程已經(jīng)擁有與 objectref 關聯(lián)的monitor,它會重新進入monitor,增加其條目計數(shù)。
- 如果另一個線程已經(jīng)擁有與 objectref 關聯(lián)的monitor,線程會阻塞,直到monitor的條目計數(shù)為零,然后再次嘗試獲得所有權
同理,執(zhí)行monitorexit 的線程必須是與objectref 引用的實例關聯(lián)的monitor的所有者。該線程遞減與objectref 關聯(lián)的monitor的入口計數(shù),如果結果條目計數(shù)的值為零,則線程退出monitor并且不再是其所有者。
在了解monitor之前,還需先大概了解對象頭這個概念。
2,對象頭
在hotspot虛擬機中,對象在內存的分布分為3個部分:對象頭,實例數(shù)據(jù),和對齊填充。

實例變量:存放類的屬性數(shù)據(jù)信息。 包括父類的屬性信息,如果是數(shù)組的實例部分還包括數(shù)組的長度,這部分內存按4字節(jié)對齊。
填充數(shù)據(jù):用于保證對象8字節(jié)對齊。 由于虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊。
對象頭:jvm采用2個字寬(Word)存儲對象頭,若對象為數(shù)組則采用3個字寬來存儲。在32位虛擬機中1字寬等于4字節(jié),64位虛擬機中1字寬等于8字節(jié)。synchronized使用的鎖對象是存儲在Java對象頭里的,jvm中采用2個字來存儲對象頭,如果對象是數(shù)組則會分配3個字,多出來的1個字記錄的是數(shù)組長度,其結構說明如下表:
| 長度 | 頭對象結構 | 說明 |
|---|---|---|
| 32/64bit | Mark Word | 存儲對象的hashCode、鎖信息或分代年齡或GC標志等信息 |
| 32/64bit | Class Metadata Address | 類型指針指向對象的類元數(shù)據(jù),JVM通過這個指針確定該對象是哪個類的實例。 |
| 32/32bit | Array length | 數(shù)組的長度(若當前對象為數(shù)組) |
由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關系的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設計成為一個非固定的數(shù)據(jù)結構,以便存儲更多有效的數(shù)據(jù)。64位JVM下,如下所示;
| 鎖狀態(tài) | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit 鎖標志位 |
|---|---|---|---|---|
| 無鎖狀態(tài) | 對象HashCode | 對象分代年齡 | 0 | 01 |
它會根據(jù)對象本身的狀態(tài)復用自己的存儲空間,如32位JVM下,除了上述列出的Mark Word默認存儲結構外,還有如下可能變化的結構:

monitor對象存在于每個Java對象的對象頭中,synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因。
3,monitor
指向互斥量的指針指向的就是monitor對象的起始地址。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結構如下
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數(shù)
_waiters = 0, //等待線程數(shù)
_recursions = 0; //重入次數(shù)
_object = NULL;//存儲該monitor的對象
_owner = NULL;//指向獲得monitor的ObjectWaiter對象
_WaitSet = NULL; //處于wait狀態(tài)的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;//多線程競爭鎖時的單向列表
FreeNext = NULL ;
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有兩個隊列,WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 后進入 _Owner 區(qū)域并把monitor中的owner變量設置為當前線程同時monitor中的計數(shù)器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復為null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執(zhí)行完畢也將釋放monitor(鎖)并復位變量的值,以便其他線程進入獲取monitor(鎖)。
如下圖所示,一個線程通過1號門進入Entry Set(入口區(qū)),如果在入口區(qū)沒有線程等待,那么這個線程就會獲取監(jiān)視器成為監(jiān)視器的Owner,然后執(zhí)行監(jiān)視區(qū)域的代碼。如果在入口區(qū)中有其它線程在等待,那么新來的線程也會和這些線程一起等待。線程在持有監(jiān)視器的過程中,有兩個選擇,一個是正常執(zhí)行監(jiān)視器區(qū)域的代碼,釋放監(jiān)視器,通過5號門退出監(jiān)視器;還有可能等待某個條件的出現(xiàn),于是它會通過3號門到Wait Set(等待區(qū))休息,直到相應的條件滿足后再通過4號門進入重新獲取監(jiān)視器再執(zhí)行。
當一個線程釋放監(jiān)視器時,在入口區(qū)和等待區(qū)的等待線程都會去競爭監(jiān)視器,如果入口區(qū)的線程贏了,會從2號門進入;如果等待區(qū)的線程贏了會從4號門進入。只有通過3號門才能進入等待區(qū),在等待區(qū)中的線程只有通過4號門才能退出等待區(qū),也就是說一個線程只有在持有監(jiān)視器時才能執(zhí)行wait操作,處于等待的線程只有再次獲得監(jiān)視器才能退出等待狀態(tài)。

monitor并不是隨著對象創(chuàng)建而創(chuàng)建的。而是每個線程都存在兩個ObjectMonitor對象列表,分別為free和used列表;同時jvm中也維護著global locklist。當線程需要ObjectMonitor對象時,首先從自身的free表中申請,若存在則使用,若不存在則從global list中申請。
monitor是線程私有的數(shù)據(jù)結構,每一個線程都有一個可用monitor列表,同時還有一個全局的可用列表,monitor的內部如下所示,

Owner:初始時為NULL表示當前沒有任何線程擁有該monitor,當線程成功擁有該鎖后保存線程唯一標識,當鎖被釋放時又設置為NULL;
EntryQ:關聯(lián)一個系統(tǒng)互斥鎖(semaphore),阻塞所有試圖鎖住monitor失敗的線程。
RcThis:表示blocked或waiting在該monitor上的所有線程的個數(shù)。
Nest:用來實現(xiàn)重入鎖的計數(shù)。
HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值:0表示沒有需要喚醒的線程,1表示要喚醒一個繼任線程來競爭鎖。
4,小結
JVM 是通過進入、退出 對象監(jiān)視器(Monitor) 來實現(xiàn)對方法、同步塊的同步的,而對象監(jiān)視器的本質依賴于底層操作系統(tǒng)的 互斥鎖(Mutex Lock) 實現(xiàn)。具體實現(xiàn)是在編譯之后在同步方法調用前加入一個monitor.enter指令,在退出方法和異常處插入monitor.exit的指令。對于沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程monitor.exit之后才能嘗試繼續(xù)獲取鎖。
當執(zhí)行monitorenter指令時,線程試圖獲取鎖也就是獲取monitor的持有權。當計數(shù)器為0則可以成功獲取,獲取后將鎖計數(shù)器設為1也就是加1。相應的在執(zhí)行monitorexit指令后,將鎖計數(shù)器設為0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。

從synchronized的特點中可以看到它是一種重量級鎖,會涉及到操作系統(tǒng)狀態(tài)的切換影響效率,所以JDK1.6中對synchronized進行了各種優(yōu)化,為了能減少獲取和釋放鎖帶來的消耗引入了偏向鎖和輕量鎖。
七,鎖機制
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現(xiàn)鎖的降級。
Mark Word中的數(shù)據(jù)隨著鎖標志位的變化而變化,如下

1,偏向鎖
偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優(yōu)化手段。經(jīng)過研究發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是被同一線程多次獲得,因此為了減少這同一線程獲取鎖的代價而引入偏向鎖(看來社會上的二八法則也存在于這里)。
偏向鎖的獲?。寒斠粋€線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解釋,只需要簡單地測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果是,則直接獲得鎖,執(zhí)行同步塊;如果不是,則使用CAS操作更改線程ID,更改成功獲得鎖,更改失敗開始撤銷偏向鎖。
偏向鎖的釋放:偏向鎖只有存在鎖競爭的情況下才會釋放。撤銷偏向鎖需要等待全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼),首先暫停擁有偏向鎖的線程,然后檢查此線程是否活著,如果線程不處于活動狀態(tài),則轉成無鎖狀態(tài);如果還活著,升級為輕量級鎖。下圖線程1展示了偏向鎖獲取的過程,線程2展示了偏向鎖撤銷的過程。

偏向鎖的關閉:偏向鎖在Java 6和Java 7里是默認啟用的,但是它在應用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認會進入輕量級鎖狀態(tài)。
對于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。
2,輕量級鎖
倘若偏向鎖失敗,虛擬機并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優(yōu)化手段(1.6之后加入的),此時Mark Word 的結構也變?yōu)檩p量級鎖的結構。輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用產生的性能消耗。輕量級鎖能夠提升程序性能的依據(jù)是“對絕大部分的鎖,在整個同步周期內都不存在競爭”,注意這是經(jīng)驗數(shù)據(jù)。需要了解的是,輕量級鎖所適應的場景是線程交替執(zhí)行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。
輕量鎖的獲?。壕€程在執(zhí)行同步塊之前,JVM會先在當前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
輕量鎖的釋放:輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個線程同時爭奪鎖,導致鎖膨脹的流程圖。

3,重量級鎖
因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態(tài)。當鎖處于這個狀態(tài)下,其他線程試圖獲取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。
4,小結
| 鎖 | 優(yōu)點 | 缺點 | 適用場景 |
|---|---|---|---|
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級的差距 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用于只有一個線程訪問同步塊場景(只有一個線程進入臨界區(qū)) |
| 輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應速度 | 如果始終得不到索競爭的線程,使用自旋會消耗CPU | 追求響應速度,同步塊執(zhí)行速度非??欤ǘ鄠€線程交替進入臨界區(qū)) |
| 重量級鎖 | 線程競爭不使用自旋,不會消耗CPU | 線程阻塞,響應時間緩慢 | 追求吞吐量,同步塊執(zhí)行速度較慢(多個線程同時進入臨界區(qū)) |
八,拓展
1,CAS操作
使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設每一次執(zhí)行臨界區(qū)代碼都會產生沖突,所以當前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖。而CAS操作(又稱為無鎖操作)是一種樂觀鎖策略,它假設所有線程訪問共享資源的時候不會出現(xiàn)沖突,既然不會出現(xiàn)沖突自然而然就不會阻塞其他線程的操作。因此,線程就不會出現(xiàn)阻塞停頓的狀態(tài)。那么,如果出現(xiàn)沖突了怎么辦?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑒別線程是否出現(xiàn)沖突,出現(xiàn)沖突就重試當前操作直到?jīng)]有沖突為止。
CAS包含三個值:V 內存地址存放的實際值;O 預期的值(舊值);N 更新的新值。當V和O相同時,也就是說舊值和內存中實際的值相同表明該值沒有被其他線程更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,表明該值已經(jīng)被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當多個線程使用CAS操作一個變量是,只有一個線程會成功,并成功更新,其余會失敗。失敗的線程會重新嘗試,當然也可以選擇掛起線程。
簡單來說,就是CPU去更新一個值,但如果想改的值不再是原來的值,操作就失敗,因為很明顯,有其它操作先改變了這個值。就是指當兩者進行比較時,如果相等,則證明共享數(shù)據(jù)沒有被修改,替換成新值,然后繼續(xù)往下運行;如果不相等,說明共享數(shù)據(jù)已經(jīng)被修改,放棄已經(jīng)所做的操作,然后重新執(zhí)行剛才的操作。容易看出 CAS 操作是基于共享數(shù)據(jù)不會被修改的假設,采用了類似于數(shù)據(jù)庫的commit-retry 的模式。當同步?jīng)_突出現(xiàn)的機會很少時,這種假設能帶來較大的性能提升。
2,CAS問題
1,ABA問題
因為CAS會檢查舊值有沒有變化,這里存在這樣一個有意思的問題。比如一個舊值A變?yōu)榱顺葿,然后再變成A,剛好在做CAS時檢查發(fā)現(xiàn)舊值并沒有變化依然為A,但是實際上的確發(fā)生了變化。解決方案可以沿襲數(shù)據(jù)庫中常用的樂觀鎖方式,添加一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C。
2,自旋時間過長
使用CAS時非阻塞同步,也就是說不會將線程掛起,會自旋(簡單來說就是一直循環(huán))進行下一次嘗試,如果這里自旋時間過長對性能是很大的消耗。
3,只能保證一個共享變量的原子操作
當對一個共享變量執(zhí)行操作時CAS能保證其原子性,如果對多個共享變量進行操作,CAS就不能保證其原子性。但可以通過新建一個類,其中的成員變量就是這幾個共享變量,然后將這個對象做CAS操作就可以保證其原子性(atomic中提供了AtomicReference來保證引用對象之間的原子性)
3,樂觀鎖
樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到并發(fā)寫的可能性低,每次去拿數(shù)據(jù)的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),采取在寫時先讀出當前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復讀-比較-寫的操作。java中的樂觀鎖基本都是通過CAS操作實現(xiàn)的,CAS是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。
4,悲觀鎖
悲觀鎖是就是悲觀思想,即認為寫多,遇到并發(fā)寫的可能性高,每次去拿數(shù)據(jù)的時候都認為別人會修改,所以每次在讀寫數(shù)據(jù)的時候都會上鎖,這樣別人想讀寫這個數(shù)據(jù)就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉換為悲觀鎖,如RetreenLock。