由淺深入理解java多線程,java并發(fā),synchronized實現(xiàn)原理及線程鎖機(jī)制
[TOC]
多進(jìn)程是指操作系統(tǒng)能同時運行多個任務(wù)(程序)。
多線程是指在同一程序中有多個順序流在執(zhí)行。
一,線程的生命周期
[圖片上傳失敗...(image-370bc0-1635087555179)]
-
新建狀態(tài):
使用 new 關(guān)鍵字和 Thread 類或其子類建立一個線程對象后,該線程對象就處于新建狀態(tài)。它保持這個狀態(tài)直到程序 start() 這個線程。
-
就緒狀態(tài):
當(dāng)線程對象調(diào)用了start()方法之后,該線程就進(jìn)入就緒狀態(tài)。就緒狀態(tài)的線程處于就緒隊列中,要等待JVM里線程調(diào)度器的調(diào)度。
-
運行狀態(tài):
如果就緒狀態(tài)的線程獲取 CPU 資源,就可以執(zhí)行 run(),此時線程便處于運行狀態(tài)。處于運行狀態(tài)的線程最為復(fù)雜,它可以變?yōu)樽枞麪顟B(tài)、就緒狀態(tài)和死亡狀態(tài)。
-
阻塞狀態(tài):
如果一個線程執(zhí)行了sleep(睡眠)、suspend(掛起)等方法,失去所占用資源之后,該線程就從運行狀態(tài)進(jìn)入阻塞狀態(tài)。在睡眠時間已到或獲得設(shè)備資源后可以重新進(jìn)入就緒狀態(tài)。可以分為三種:
- 等待阻塞:運行狀態(tài)中的線程執(zhí)行 wait() 方法,使線程進(jìn)入到等待阻塞狀態(tài)。
- 同步阻塞:線程在獲取 synchronized 同步鎖失敗(因為同步鎖被其他線程占用)。
- 其他阻塞:通過調(diào)用線程的 sleep() 或 join() 發(fā)出了 I/O 請求時,線程就會進(jìn)入到阻塞狀態(tài)。當(dāng)sleep() 狀態(tài)超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉(zhuǎn)入就緒狀態(tài)。
-
死亡狀態(tài):
一個運行狀態(tài)的線程完成任務(wù)或者其他終止條件發(fā)生時,該線程就切換到終止?fàn)顟B(tài)。
二,線程的調(diào)度
調(diào)整線程優(yōu)先級
Java線程有優(yōu)先級,優(yōu)先級高的線程會獲得較多的運行機(jī)會。
線程睡眠
Thread.sleep(long millis)方法,使線程轉(zhuǎn)到阻塞狀態(tài)。millis參數(shù)設(shè)定睡眠的時間,以毫秒為單 位。當(dāng)睡眠結(jié)束后,就轉(zhuǎn)為就緒(Runnable)狀態(tài)。sleep()平臺移植性好。
線程等待
Object類中的wait()方法,導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價于調(diào)用 wait(0) 一樣。 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價于調(diào)用 wait(0) 一樣。
線程讓步
Thread.yield() 方法,暫停當(dāng)前正在執(zhí)行的線程對象,把執(zhí)行機(jī)會讓給相同或者更高優(yōu)先級的線 程。
線程加入
join()方法,等待其他線程終止。在當(dāng)前線程中調(diào)用另一個線程的join()方法,則當(dāng)前線程轉(zhuǎn)入阻 塞狀態(tài),直到另一個進(jìn)程運行結(jié)束,當(dāng)前線程再由阻塞轉(zhuǎn)為就緒狀態(tài)。
線程喚醒
Object類中的notify()方法,喚醒在此對象監(jiān)視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,并在對實現(xiàn)做出決定時發(fā)生。線程通過調(diào)用其中一個 wait 方法,在對象的監(jiān)視器上等待。 直到當(dāng)前的線程放棄此對象上的鎖定,才能繼續(xù)執(zhí)行被喚醒的線程。被喚醒的線程將以常規(guī)方式與在該對象上主動同步的其他所有線程進(jìn)行競爭;例如,喚醒的線程在作為鎖定此對象 的下一個線程方面沒有可靠的特權(quán)或劣勢。類似的方法還有一個notifyAll(),喚醒在此對象監(jiān)視器上等待的所有線程。
三,創(chuàng)建多線程的方式
1,通過實現(xiàn)Runnable接口
//
public class T3 implements Runnable {
String a;
//構(gòu)造方法
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 類的構(gòu)造方法 Thread(Runnable target) 構(gòu)造出對象,然后調(diào)用 Thread 對象的 start() 方法來運行多線程代碼。
輸出結(jié)果:
結(jié)果1 結(jié)果2
北京 上海
上海 北京
2,通過繼承Thread類
//
public class T1 extends Thread {
String a;
//構(gòu)造方法
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();
}
}
輸出結(jié)果:
結(jié)果1 結(jié)果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)重復(fù)的數(shù)值,實現(xiàn)數(shù)據(jù)共享。部分輸出結(jié)果為:
輸出結(jié)果1 | 輸出結(jié)果2 | 輸出結(jié)果3 | 輸出結(jié)果4 | 輸出結(jié)果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();
}
}
輸出結(jié)果
兩個線程,一起操作同一個數(shù)值,每個線程各操作5次,出現(xiàn)了重復(fù)的數(shù)值,未實現(xiàn)數(shù)據(jù)共享。部分輸出結(jié)果為:
輸出結(jié)果1 | 輸出結(jié)果2 | 輸出結(jié)果3 | 輸出結(jié)果4 | 輸出結(jié)果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 |
總結(jié)
實現(xiàn) Runnable 接口比繼承 Thread 類所具有的優(yōu)勢:
1):適合多個相同的程序代碼的線程去處理同一個資源
2):可以避免 java 中的單繼承的限制
五,synchronized實現(xiàn)多線程數(shù)據(jù)共享
當(dāng)存在多個線程操作共享數(shù)據(jù)時,需要保證同一時刻有且只有一個線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行。
當(dāng)兩個并發(fā)線程訪問同一個對象中的 synchronized 代碼塊時,在同一時刻只能有一個線程得到執(zhí)行,另一個線程受阻塞,必須等待當(dāng)前線程執(zhí)行完這個代碼塊以后才能執(zhí)行該代碼塊。此時線程是互斥的,因為在執(zhí)行代碼塊時會鎖定當(dāng)前的對象,只有執(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();
}
}
輸出結(jié)果;參照線程間數(shù)據(jù)共享的Runnable接口的輸出結(jié)果。可實現(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();
}
}
輸出結(jié)果;參照線程間數(shù)據(jù)共享的Thread類的輸出結(jié)果。沒有實現(xiàn)數(shù)據(jù)共享
如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當(dāng)前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當(dāng)前對象鎖是obj2),這樣是允許的,因為兩個實例對象鎖并不同相同, 此時如果兩個線程操作數(shù)據(jù)并非共享的。
雖然我們使用synchronized修飾了 aaa 方法,但卻new了兩個不同的實例對象,這也就意味著存在著兩個不同的實例對象鎖,因此t1和t2都會進(jìn)入各自的對象鎖,也就是說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();
}
}
輸出結(jié)果;參照線程間數(shù)據(jù)共享的Runnable接口的輸出結(jié)果。可實現(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();
}
}
輸出結(jié)果;參照線程間數(shù)據(jù)共享的Runnable接口的輸出結(jié)果。可實現(xiàn)數(shù)據(jù)共享
synchronized作用于靜態(tài)的 aaa 方法,這樣的話,對象鎖就當(dāng)前類對象,由于無論創(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,總結(jié)
start()方法的調(diào)用后并不是立即執(zhí)行多線程代碼,而是使得該線程變?yōu)榭蛇\行態(tài)(Runnable),什么時候運行是由操作系統(tǒng)決定的。
請記住,上下文的切換開銷也很重要,如果你創(chuàng)建了太多的線程,CPU 花費在上下文的切換的時間將多于執(zhí)行程序的時間!
- 沒有 synchronized 關(guān)鍵字的默認(rèn)情況。如線程間數(shù)據(jù)共享一節(jié)中并沒有用該關(guān)鍵字。
- 實例化多個對象,也就存在多個對象鎖,每個線程用不同的對象鎖,數(shù)據(jù)自然無法共享。
- 不管實例化多少個對象,如果synchronized作用于靜態(tài)方法,由于靜態(tài)的特殊性,該對象只會有一個,那么在這樣的情況下對象鎖又是唯一的。
六,synchronized實現(xiàn)原理
1,synchronized修飾后的字節(jié)碼
上述synchronized主要是了解數(shù)據(jù)共享的,其字節(jié)碼并不直觀看鎖相關(guān)的,另外寫了個如下所示;
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é)碼內(nèi)容
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é)碼可知,當(dāng)修飾方法時,JVM采用 ACC_SYNCHRONIZED
標(biāo)記符來實現(xiàn)同步。 當(dāng)修飾類時,JVM采用monitorenter、monitorexit兩個指令來實現(xiàn)同步。(在字節(jié)碼里面可以看見在修飾類時,有Exception table,這是因為,JVM會自動在synchronized代碼塊中加入異常捕獲,從而保證代碼拋出異常時,仍能夠釋放當(dāng)前線程占用的鎖,避免出現(xiàn)死鎖現(xiàn)象。)
在synchronized修飾方法時是添加ACC_SYNCHRONIZED
標(biāo)識。方法級同步是隱式執(zhí)行的,作為方法調(diào)用和返回的一部分。 同步方法在運行時常量池的 method_info 結(jié)構(gòu)中通過 ACC_SYNCHRONIZED 標(biāo)志進(jìn)行區(qū)分,該標(biāo)志由方法調(diào)用指令檢查。 當(dāng)調(diào)用設(shè)置了 ACC_SYNCHRONIZED 的方法時,執(zhí)行線程進(jìn)入監(jiān)視器(monitor),調(diào)用方法本身,并退出monitor,無論方法調(diào)用是正常完成還是突然完成。 在執(zhí)行線程擁有monitor期間,沒有其他線程可以進(jìn)入它。 如果在調(diào)用同步方法過程中拋出異常并且同步方法沒有處理該異常,則在異常重新拋出同步方法之前,該方法的monitor會自動退出。
在synchronized修飾類時是通過monitorenter、monitorexit指令。 當(dāng)且僅當(dāng)monitor有所有者時,monitor才被鎖定。 執(zhí)行monitorenter 的線程嘗試獲得與objectref 關(guān)聯(lián)的monitor的所有權(quán),如下所示:
- 如果與objectref 關(guān)聯(lián)的monitor的條目計數(shù)為零,則該線程進(jìn)入monitor并將其條目計數(shù)設(shè)置為1,然后該線程是monitor的所有者。
- 如果線程已經(jīng)擁有與 objectref 關(guān)聯(lián)的monitor,它會重新進(jìn)入monitor,增加其條目計數(shù)。
- 如果另一個線程已經(jīng)擁有與 objectref 關(guān)聯(lián)的monitor,線程會阻塞,直到monitor的條目計數(shù)為零,然后再次嘗試獲得所有權(quán)
同理,執(zhí)行monitorexit 的線程必須是與objectref 引用的實例關(guān)聯(lián)的monitor的所有者。該線程遞減與objectref 關(guān)聯(lián)的monitor的入口計數(shù),如果結(jié)果條目計數(shù)的值為零,則線程退出monitor并且不再是其所有者。
在了解monitor之前,還需先大概了解對象頭這個概念。
2,對象頭
在hotspot虛擬機(jī)中,對象在內(nèi)存的分布分為3個部分:對象頭,實例數(shù)據(jù),和對齊填充。
實例變量:存放類的屬性數(shù)據(jù)信息。 包括父類的屬性信息,如果是數(shù)組的實例部分還包括數(shù)組的長度,這部分內(nèi)存按4字節(jié)對齊。
填充數(shù)據(jù):用于保證對象8字節(jié)對齊。 由于虛擬機(jī)要求對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊。
對象頭:jvm采用2個字寬(Word)存儲對象頭,若對象為數(shù)組則采用3個字寬來存儲。在32位虛擬機(jī)中1字寬等于4字節(jié),64位虛擬機(jī)中1字寬等于8字節(jié)。synchronized使用的鎖對象是存儲在Java對象頭里的,jvm中采用2個字來存儲對象頭,如果對象是數(shù)組則會分配3個字,多出來的1個字記錄的是數(shù)組長度,其結(jié)構(gòu)說明如下表:
長度 | 頭對象結(jié)構(gòu) | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲對象的hashCode、鎖信息或分代年齡或GC標(biāo)志等信息 |
32/64bit | Class Metadata Address | 類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過這個指針確定該對象是哪個類的實例。 |
32/32bit | Array length | 數(shù)組的長度(若當(dāng)前對象為數(shù)組) |
由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關(guān)系的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設(shè)計成為一個非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲更多有效的數(shù)據(jù)。64位JVM下,如下所示;
鎖狀態(tài) | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit 鎖標(biāo)志位 |
---|---|---|---|---|
無鎖狀態(tài) | 對象HashCode | 對象分代年齡 | 0 | 01 |
它會根據(jù)對象本身的狀態(tài)復(fù)用自己的存儲空間,如32位JVM下,除了上述列出的Mark Word默認(rèn)存儲結(jié)構(gòu)外,還有如下可能變化的結(jié)構(gòu):
monitor對象存在于每個Java對象的對象頭中,synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因。
3,monitor
指向互斥量的指針指向的就是monitor對象的起始地址。在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下
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對象的線程,當(dāng)多個線程同時訪問一段同步代碼時,首先會進(jìn)入 _EntryList 集合,當(dāng)線程獲取到對象的monitor 后進(jìn)入 _Owner 區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時monitor中的計數(shù)器count加1,若線程調(diào)用 wait() 方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時該線程進(jìn)入 WaitSet集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。
如下圖所示,一個線程通過1號門進(jìn)入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ū))休息,直到相應(yīng)的條件滿足后再通過4號門進(jìn)入重新獲取監(jiān)視器再執(zhí)行。
當(dāng)一個線程釋放監(jiān)視器時,在入口區(qū)和等待區(qū)的等待線程都會去競爭監(jiān)視器,如果入口區(qū)的線程贏了,會從2號門進(jìn)入;如果等待區(qū)的線程贏了會從4號門進(jìn)入。只有通過3號門才能進(jìn)入等待區(qū),在等待區(qū)中的線程只有通過4號門才能退出等待區(qū),也就是說一個線程只有在持有監(jiān)視器時才能執(zhí)行wait操作,處于等待的線程只有再次獲得監(jiān)視器才能退出等待狀態(tài)。
monitor并不是隨著對象創(chuàng)建而創(chuàng)建的。而是每個線程都存在兩個ObjectMonitor對象列表,分別為free和used列表;同時jvm中也維護(hù)著global locklist。當(dāng)線程需要ObjectMonitor對象時,首先從自身的free表中申請,若存在則使用,若不存在則從global list中申請。
monitor是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個線程都有一個可用monitor列表,同時還有一個全局的可用列表,monitor的內(nèi)部如下所示,
Owner:初始時為NULL表示當(dāng)前沒有任何線程擁有該monitor,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識,當(dāng)鎖被釋放時又設(shè)置為NULL;
EntryQ:關(guān)聯(lián)一個系統(tǒng)互斥鎖(semaphore),阻塞所有試圖鎖住monitor失敗的線程。
RcThis:表示blocked或waiting在該monitor上的所有線程的個數(shù)。
Nest:用來實現(xiàn)重入鎖的計數(shù)。
HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導(dǎo)致性能嚴(yán)重下降。Candidate只有兩種可能的值:0表示沒有需要喚醒的線程,1表示要喚醒一個繼任線程來競爭鎖。
4,小結(jié)
JVM 是通過進(jìn)入、退出 對象監(jiān)視器(Monitor) 來實現(xiàn)對方法、同步塊的同步的,而對象監(jiān)視器的本質(zhì)依賴于底層操作系統(tǒng)的 互斥鎖(Mutex Lock) 實現(xiàn)。具體實現(xiàn)是在編譯之后在同步方法調(diào)用前加入一個monitor.enter
指令,在退出方法和異常處插入monitor.exit
的指令。對于沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程monitor.exit
之后才能嘗試?yán)^續(xù)獲取鎖。
當(dāng)執(zhí)行monitorenter
指令時,線程試圖獲取鎖也就是獲取monitor的持有權(quán)。當(dāng)計數(shù)器為0則可以成功獲取,獲取后將鎖計數(shù)器設(shè)為1也就是加1。相應(yīng)的在執(zhí)行monitorexit
指令后,將鎖計數(shù)器設(shè)為0,表明鎖被釋放。如果獲取對象鎖失敗,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
從synchronized的特點中可以看到它是一種重量級鎖,會涉及到操作系統(tǒng)狀態(tài)的切換影響效率,所以JDK1.6中對synchronized進(jìn)行了各種優(yōu)化,為了能減少獲取和釋放鎖帶來的消耗引入了偏向鎖和輕量鎖。
七,鎖機(jī)制
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現(xiàn)鎖的降級。
Mark Word中的數(shù)據(jù)隨著鎖標(biāo)志位的變化而變化,如下
1,偏向鎖
偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優(yōu)化手段。經(jīng)過研究發(fā)現(xiàn),在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是被同一線程多次獲得,因此為了減少這同一線程獲取鎖的代價而引入偏向鎖(看來社會上的二八法則也存在于這里)。
偏向鎖的獲取:當(dāng)一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時不需要進(jìn)行CAS操作來加鎖和解釋,只需要簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。如果是,則直接獲得鎖,執(zhí)行同步塊;如果不是,則使用CAS操作更改線程ID,更改成功獲得鎖,更改失敗開始撤銷偏向鎖。
偏向鎖的釋放:偏向鎖只有存在鎖競爭的情況下才會釋放。撤銷偏向鎖需要等待全局安全點(在這個時間點上沒有正在執(zhí)行的字節(jié)碼),首先暫停擁有偏向鎖的線程,然后檢查此線程是否活著,如果線程不處于活動狀態(tài),則轉(zhuǎn)成無鎖狀態(tài);如果還活著,升級為輕量級鎖。下圖線程1展示了偏向鎖獲取的過程,線程2展示了偏向鎖撤銷的過程。
偏向鎖的關(guān)閉:偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)。
對于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果,畢竟極有可能連續(xù)多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應(yīng)該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。
2,輕量級鎖
倘若偏向鎖失敗,虛擬機(jī)并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優(yōu)化手段(1.6之后加入的),此時Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級鎖的結(jié)構(gòu)。輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用產(chǎn)生的性能消耗。輕量級鎖能夠提升程序性能的依據(jù)是“對絕大部分的鎖,在整個同步周期內(nèi)都不存在競爭”,注意這是經(jīng)驗數(shù)據(jù)。需要了解的是,輕量級鎖所適應(yīng)的場景是線程交替執(zhí)行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導(dǎo)致輕量級鎖膨脹為重量級鎖。
輕量鎖的獲取:線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。
輕量鎖的釋放:輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當(dāng)前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個線程同時爭奪鎖,導(dǎo)致鎖膨脹的流程圖。
3,重量級鎖
因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當(dāng)鎖處于這個狀態(tài)下,其他線程試圖獲取鎖時,都會被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進(jìn)行新一輪的奪鎖之爭。
4,小結(jié)
鎖 | 優(yōu)點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法相比僅存在納秒級的差距 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用于只有一個線程訪問同步塊場景(只有一個線程進(jìn)入臨界區(qū)) |
輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應(yīng)速度 | 如果始終得不到索競爭的線程,使用自旋會消耗CPU | 追求響應(yīng)速度,同步塊執(zhí)行速度非常快(多個線程交替進(jìn)入臨界區(qū)) |
重量級鎖 | 線程競爭不使用自旋,不會消耗CPU | 線程阻塞,響應(yīng)時間緩慢 | 追求吞吐量,同步塊執(zhí)行速度較慢(多個線程同時進(jìn)入臨界區(qū)) |
八,拓展
1,CAS操作
使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設(shè)每一次執(zhí)行臨界區(qū)代碼都會產(chǎn)生沖突,所以當(dāng)前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖。而CAS操作(又稱為無鎖操作)是一種樂觀鎖策略,它假設(shè)所有線程訪問共享資源的時候不會出現(xiàn)沖突,既然不會出現(xiàn)沖突自然而然就不會阻塞其他線程的操作。因此,線程就不會出現(xiàn)阻塞停頓的狀態(tài)。那么,如果出現(xiàn)沖突了怎么辦?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑒別線程是否出現(xiàn)沖突,出現(xiàn)沖突就重試當(dāng)前操作直到?jīng)]有沖突為止。
CAS包含三個值:V 內(nèi)存地址存放的實際值;O 預(yù)期的值(舊值);N 更新的新值。當(dāng)V和O相同時,也就是說舊值和內(nèi)存中實際的值相同表明該值沒有被其他線程更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,表明該值已經(jīng)被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當(dāng)多個線程使用CAS操作一個變量是,只有一個線程會成功,并成功更新,其余會失敗。失敗的線程會重新嘗試,當(dāng)然也可以選擇掛起線程。
簡單來說,就是CPU去更新一個值,但如果想改的值不再是原來的值,操作就失敗,因為很明顯,有其它操作先改變了這個值。就是指當(dāng)兩者進(jìn)行比較時,如果相等,則證明共享數(shù)據(jù)沒有被修改,替換成新值,然后繼續(xù)往下運行;如果不相等,說明共享數(shù)據(jù)已經(jīng)被修改,放棄已經(jīng)所做的操作,然后重新執(zhí)行剛才的操作。容易看出 CAS 操作是基于共享數(shù)據(jù)不會被修改的假設(shè),采用了類似于數(shù)據(jù)庫的commit-retry 的模式。當(dāng)同步?jīng)_突出現(xiàn)的機(jī)會很少時,這種假設(shè)能帶來較大的性能提升。
2,CAS問題
1,ABA問題
因為CAS會檢查舊值有沒有變化,這里存在這樣一個有意思的問題。比如一個舊值A(chǔ)變?yōu)榱顺葿,然后再變成A,剛好在做CAS時檢查發(fā)現(xiàn)舊值并沒有變化依然為A,但是實際上的確發(fā)生了變化。解決方案可以沿襲數(shù)據(jù)庫中常用的樂觀鎖方式,添加一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C。
2,自旋時間過長
使用CAS時非阻塞同步,也就是說不會將線程掛起,會自旋(簡單來說就是一直循環(huán))進(jìn)行下一次嘗試,如果這里自旋時間過長對性能是很大的消耗。
3,只能保證一個共享變量的原子操作
當(dāng)對一個共享變量執(zhí)行操作時CAS能保證其原子性,如果對多個共享變量進(jìn)行操作,CAS就不能保證其原子性。但可以通過新建一個類,其中的成員變量就是這幾個共享變量,然后將這個對象做CAS操作就可以保證其原子性(atomic中提供了AtomicReference來保證引用對象之間的原子性)
3,樂觀鎖
樂觀鎖是一種樂觀思想,即認(rèn)為讀多寫少,遇到并發(fā)寫的可能性低,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),采取在寫時先讀出當(dāng)前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復(fù)讀-比較-寫的操作。java中的樂觀鎖基本都是通過CAS操作實現(xiàn)的,CAS是一種更新的原子操作,比較當(dāng)前值跟傳入值是否一樣,一樣則更新,否則失敗。
4,悲觀鎖
悲觀鎖是就是悲觀思想,即認(rèn)為寫多,遇到并發(fā)寫的可能性高,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改,所以每次在讀寫數(shù)據(jù)的時候都會上鎖,這樣別人想讀寫這個數(shù)據(jù)就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉(zhuǎn)換為悲觀鎖,如RetreenLock。