主頁 > 後端開發 > 【開發寶典】Java并發系列教程

【開發寶典】Java并發系列教程

2023-02-15 06:55:37 後端開發

作者:京東零售 劉躍明

Monitor概念

Java物件的記憶體布局

物件除了我們自定義的一些屬性外,還有其它資料,在記憶體中可以分為三個區域:物件頭、實體資料、對齊填充,這三個區域組成起來才是一個完整的物件,

物件頭:在JVM中需要大量存盤物件,存盤時為了實作一些額外的功能,需要在物件中添加一些標記欄位用于增強物件功能,這些標記欄位組成了物件頭,

實體資料:存放類的屬性資料資訊,包括父類的屬性資訊,

對齊填充:由于虛擬機要求物件其實地址必須是8位元組的整數倍,需要存在填充區域以滿足8位元組的整數倍,填充資料不是必須存在的,僅僅是為了位元組對齊,

圖1

Java物件頭

JVM中物件頭的方式有以下兩種(以32位虛擬機為例):

普通物件

Object Header (64 bits)
Mark Word (32 bits) Klass Word (32 bits)

陣列物件

Object Header (96 bits)
Mark Word(32bits) Klass Word(32bits) array length(32bits)

Mark Word

這部分主要用來存盤物件自身的運行資料,如hashcode、gc分帶年齡等,Mark Word的位長度為JVM的一個Word大小,也就是說32位JVM的Mark Word為32位,64位JVM為64位,為了讓一個字大小存盤更多的資訊,JVM將字的最低兩個位設定為標記位,不同標記位下的Mark Word示意如下:

Mark Word (32 bits) State
identity_hashcode:25 age:4 biased_lock:1 lock:2 Normal
thread:23 epoch:2 age:4 biased_lock:1 lock:2 Biased
ptr_to_lock_record:30 lock:2 LightweightLocked
ptr_to_heavyweight_monitor:30 lock:2 HeavyweightLocked
lock:2 Marked for GC

其中各部分的含義如下:

lock: 2位的鎖狀態標記位,該標記的值不同,整個Mark Word表示的含義不同,

biased_lock lock 狀態
0 01 無鎖
1 01 偏向鎖
0 00 輕量級鎖
0 10 重量級鎖
0 11 GC標記

biased_lock: 物件是否啟用偏向鎖標記,只占1個二進制位,為1時表示物件啟用偏向鎖,為0時表示物件沒有偏向鎖,

age: 4位的Java物件年齡,在GC中,如果物件再Survivor區復制一次,年齡增加1,當物件達到設定的閾值時,將會晉升到老年代,默認情況下,并行GC的年齡閾值為15,并發GC的年齡閾值為6,由于age只有4位,所以最大值為15,這就是-XX:MaxTenuringThreshold選項最大值為15的原因,

identity_hashcode: 25位的物件表示Hash碼,采用延遲加載技術,呼叫方法System.idenHashcode()計算,并會將結果寫到該物件頭中,當物件被鎖定時,該值會移動到管程Monitor中,

thread: 持有偏向鎖的執行緒ID,

epoch: 偏向時間戳,

ptr_to_lock_record: 指向堆疊中鎖記錄的指標,

ptr_to_heavyweight_monitor: 指向管程Monitor的指標,

Klass Word

這一部分用于存盤物件的型別指標,該指標指向它的類元資料,JVM通過這個指標確定物件是哪個類的實體,該指標的位長度為JVM的一個字大小,即32位的JVM為32位,64位的JVM為64位,

array length

如果物件是一個陣列,那么物件頭還需要有額外的空間用于存盤陣列的長度,這部分資料的長度也隨著JVM架構的不同而不同:32位的JVM長度為32位,64位JVM則為64位,

Monitor原理

Monitor被翻譯為監視器管程

每個Java物件都可以關聯一個Monitor物件,如果使用synchronized給物件上鎖(重量級)之后,該物件頭的Mark Word中就被設定指向Monitor物件的指標,

Monitor結構如下:

圖2

?剛開始Monitor中Owner為null

?當Thread-2執行synchronized(obj)就會將Monitor的所有者Owner置為Thread-2,Monitor中只能有一個Owner

?在Thread-2上鎖的程序中,如果Thread-3、Thread-4、Thread-5也來執行synchronized(obj),就會進入EntryList BLOCKED

?Thread-2執行完同步代碼塊的內容,然后喚醒EntryList中等待的執行緒來競爭鎖,競爭是非公平的,也就是先進并非先獲取鎖

?圖2中WaitSet中的Thread-0、Thread-1是之前獲得過鎖,但條件不滿足進入WAITING狀態的執行緒,后面講wait-notify時會分析

注意:

?synchronized必須是進入同一個物件的Monitor才有上述的效果

?不加synchronized的物件不會關聯監視器,不遵從以上規則

synchronized原理

static final Object lock = new Object();
static int counter = 0;

public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}

對應的位元組碼為:

public static main([Ljava/lang/String;)V

TRYCATCHBLOCK L0 L1 L2 null

TRYCATCHBLOCK L2 L3 L2 null

L4

LINENUMBER 6 L4

GETSTATIC MyClass03.lock : Ljava/lang/Object;

DUP

ASTORE 1

MONITORENTER //注釋1

L0

LINENUMBER 7 L0

GETSTATIC MyClass03.counter : I

ICONST_1

IADD

PUTSTATIC MyClass03.counter : I

L5

LINENUMBER 8 L5

ALOAD 1

MONITOREXIT //注釋2

L1

GOTO L6

L2

FRAME FULL [[Ljava/lang/String; java/lang/Object] [java/lang/Throwable]

ASTORE 2

ALOAD 1

MONITOREXIT //注釋3

L3

ALOAD 2

ATHROW

L6

LINENUMBER 9 L6

FRAME CHOP 1

RETURN

L7

LOCALVARIABLE args [Ljava/lang/String; L4 L7 0

MAXSTACK = 2

MAXLOCALS = 3

注釋1

MONITORENTER的意思為:每個物件都有一個監視鎖(Monitor),當Monitor被占用時就會處于鎖定狀態,執行緒執行MONITORENTER指令時嘗試獲取Monitor的所有權,程序如下:

?如果Monitor的進入數為0,則該執行緒進入Monitor,并將進入數設定為1,該執行緒即為Monitor的所有者(Owner)

?如果該執行緒已經占用Monitor,只是重新進入Monitor,則進入Monitor的進入數加1

?如果其它執行緒已經占用Monitor,則該執行緒進入阻塞狀態,直到Monitor進入數為0,再重新嘗試獲取Monitor的所有權

注釋2

MONITOREXIT的意思為:執行指令時,Monitor的進入數減1,如果減1后進入數為0,該執行緒退出Monitor,不再是這個Monitor的所有者,其它被Monitor阻塞的執行緒重新嘗試獲取Monitor的所有權,

總結

通過注釋1和注釋2可知,synchronized的實作原理,底層是通過Monitor的物件來完成,其實wait和notify等方法也依賴Monitor,這就是為什么wait和notify方法必須要在同步方法內呼叫,否則會拋出java.lang.IllegalMonitorStateException的原因,

如果程式正常執行則按上述描述即可完成,如果程式在同步方法內發生例外,代碼則會走注釋3,在注釋3可以看到MONITOREXIT指令,也就是synchronized已經處理例外情況下的退出,

注:方法級別的synchronized不會在位元組碼指令中有所體現,而是在常量池中增加了ACC_SYNCHRONIZED識別符號,JVM就是通過該識別符號來實作同步的,方法呼叫時,JVM會判斷方法的ACC_SYNCHRONIZED是否被設定,如果被設定,執行緒執行方法前會先獲取Monitor所有權,執行完方法后再釋放Monitor所有權,本質是一樣的,

synchronized原理進階

輕量級鎖

輕量級鎖的使用場景:如果一個物件雖然有多執行緒要加鎖,但加鎖的時間是錯開的(也就是沒有競爭),那么可以使用輕量級鎖來優化,

輕量級鎖對使用者是透明的,即語法仍然是synchronized

假設有兩個方法同步塊,利用同一個物件加鎖

static final Object obj = new Object();

public static void method1() {
    synchronized (obj) { // 同步塊 A
        method2();
    }
}

public static void method2() {
    synchronized (obj) { // 同步塊 B
    }
}

創建鎖記錄(Lock Record)物件,每個執行緒的堆疊幀都會包含一個鎖記錄的結構,內部可以存盤鎖定物件的Mark Word

圖3

讓鎖記錄中Object reference指向鎖物件,并嘗試用cas替換Object的Mark Word,將Mark Word的值存入鎖記錄

圖4

如果cas替換成功,物件頭中存盤了鎖記錄地址和狀態00,表示由該執行緒給物件加鎖,這是圖示如下

圖5

如果cas失敗,有兩種情況

?如果是其它執行緒已經持有了該Object的輕量級鎖,這是表明有競爭,進入鎖膨脹程序

?如果是自己執行緒執行了synchronized鎖重入,那么再添加一條Lock Record作為重入的技術

圖6

當退出synchronized代碼塊(解鎖時),如果有取值為null的鎖記錄,表示由重入,這是重置鎖記錄,表示重入技術減一

圖7

當退出synchronized代碼塊(解鎖時),鎖記錄的值不為null,這時使用cas將Mark Word的值回復給物件頭

?成功,則解鎖成功

?失敗,說明輕量級鎖進行了鎖膨脹或已經升級為重量級鎖,進入重量級鎖解鎖流程

鎖膨脹

如果在嘗試加輕量級鎖的程序中,CAS操作無法成功,這是一種情況就是有其它執行緒為此物件加上了輕量級鎖(有競爭),這是需要進行鎖膨脹,將輕量級鎖變為重量級鎖,

當Thread-1進行輕量級加鎖時,Thread-0已經對該物件加了輕量級鎖

圖8

這是Thread-1加輕量級鎖失敗,進入鎖膨脹流程

?即為Object物件申請Monitor鎖,讓Object指向重量級鎖地址

?然后自己進入Monitor的EntryList BLOCKED

圖9

當Thread-0退出同步塊解鎖時,使用cas將Mark Word的值恢復給物件頭,失敗,這是會進入重量級解鎖流程,即按照Monitor地址找到Monitor物件,設定Owner為null,喚醒EntryList 中BLOCKED執行緒

自旋優化

重量級鎖競爭的時候,還可以使用自旋來進行優化,如果當前執行緒自旋成功(即這時候持鎖執行緒已經退出了同步,釋放了鎖),這是當前執行緒就可以避免阻塞,

自旋重試成功的情況

執行緒1(core 1上) 物件Mark 執行緒2(core 2 上)
- 10(重量鎖) -
訪問同步塊,獲取Monitor 10(重量鎖)重量鎖指標 -
成功(加鎖) 10(重量鎖)重量鎖指標 -
執行同步塊 10(重量鎖)重量鎖指標 -
執行同步塊 10(重量鎖)重量鎖指標 訪問同步塊,獲取Monitor
執行同步塊 10(重量鎖)重量鎖指標 自旋重試
執行完畢 10(重量鎖)重量鎖指標 自旋重試
成功(解鎖) 01(無鎖) 自旋重試
- 10(重量鎖)重量鎖指標 成功(加鎖)
- 10(重量鎖)重量鎖指標 執行同步塊
-

自旋重試失敗的情況

執行緒1(core 1上) 物件Mark 執行緒2(core 2 上)
- 10(重量鎖) -
訪問同步塊,獲取Monitor 10(重量鎖)重量鎖指標 -
成功(加鎖) 10(重量鎖)重量鎖指標 -
執行同步塊 10(重量鎖)重量鎖指標 -
執行同步塊 10(重量鎖)重量鎖指標 訪問同步塊,獲取Monitor
執行同步塊 10(重量鎖)重量鎖指標 自旋重試
執行同步塊 10(重量鎖)重量鎖指標 自旋重試
執行同步塊 10(重量鎖)重量鎖指標 自旋重試
執行同步塊 10(重量鎖)重量鎖指標 阻塞
-

?自旋會占用CPU時間,單核CPU自旋就是浪費,多核CPU自旋才能發揮優勢,

?在Java 6之后自旋鎖是自適應的,比如物件剛剛的一次自旋操作成功過,那么認為這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,總之,比較智能,

?Java 7之后不能控制是否開啟自旋功能,

偏向鎖

輕量級鎖在沒有競爭時(就自己這個執行緒),每次重入仍然需要執行CAS操作,

Java 6中引入了偏向鎖做進一步優化:只有第一次使用CAS將執行緒ID設定到物件的Mark Word頭,之后發現這個執行緒ID是自己的就表示沒有競爭,不用重新CAS,以后只要不發生競爭,這個物件就歸該執行緒所有,

注:

Java 15之后廢棄偏向鎖,默認是關閉,如果想使用偏向鎖,配置-XX:+UseBiasedLocking啟動引數,

啟動偏向鎖之后,偏向鎖有一個延遲生效的機制,這是因為JVM啟動時會進行一系列的復雜活動,比如裝載配置,系統類初始化等等,在這個程序中會使用大量synchronized關鍵字對物件加鎖,且這些鎖大多數都不是偏向鎖,為了減少初始化時間,JVM默認延時加載偏向鎖,這個延時的時間大概為4s左右,具體時間因機器而異,當然我們也可以設定JVM引數 -XX:BiasedLockingStartupDelay=0 來取消延時加載偏向鎖,

例如:

static final Object obj = new Object();

public static void m1() {
    synchronized (obj) { // 同步塊 A
        m2();
    }
}

public static void m2() {
    synchronized (obj) { // 同步塊 B
        m3();
    }
}

public static void m3() {
    synchronized (obj) {
    }
}

如果關閉偏向鎖,使用輕量鎖情況:

圖10

開啟偏向鎖,使用偏向鎖情況:

圖11

偏向狀態

回憶一下物件頭格式

Mark Word (32 bits) State
identity_hashcode:25 age:4 biased_lock:1 lock:2 Normal
thread:23 epoch:2 age:4 biased_lock:1 lock:2 Biased
ptr_to_lock_record:30 lock:2 LightweightLocked
ptr_to_heavyweight_monitor:30 lock:2 HeavyweightLocked
lock:2 Marked for GC

一個物件創建時:

?如果開啟了偏向鎖(默認開啟),那么物件創建后,Mark Word值為0x05,也就是最后是3位為101,這是它的thread、epoch、age都為0

?如果沒有開啟偏向鎖,那么物件創建后,Mark Word值為0x01,也就是最后3位為001,這時它的hashcode、age都為0,第一次用到hashcode時才會賦值

我們來驗證下,使用jol第三方工具,以及對工具列印物件頭做了一個處理,讓物件頭開起來更簡便:

測驗代碼

public synchronized static void main(String[] args){
    log.info("{}", toSimplePrintable(object));
}

開啟偏向鎖的情況下

列印的資料如下(由于Java15之后偏向鎖廢棄,因此打開偏向鎖列印會警告)

17:15:17 [main] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

最后為101,其他都為0,驗證了上述第一條,

可能你又要問了,我這也沒使用synchronized關鍵字呀,那不也應該是無鎖么?怎么會是偏向鎖呢?

仔細看一下偏向鎖的組成,對照輸出結果紅色劃線位置,你會發現占用 thread 和 epoch 的 位置的均為0,說明當前偏向鎖并沒有偏向任何執行緒,此時這個偏向鎖正處于可偏向狀態,準備好進行偏向了!你也可以理解為此時的偏向鎖是一個特殊狀態的無鎖

關閉偏向鎖的情況下

列印的資料如下

17:18:32 [main] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

最后為001,其它都是0,驗證了上述第二條,

接下來驗證加鎖的情況,代碼如下:

private static Object object = new Object();
public synchronized static void main(String[] args){
    new Thread(()->{
        log.info("{}", "synchronized前");
        log.info("{}", toSimplePrintable(object));
        synchronized (object){
            log.info("{}", "synchronized中");
            log.info("{}", toSimplePrintable(object));
        }
        log.info("{}", "synchronized后");
        log.info("{}", toSimplePrintable(object));
    },"t1").start();
}

開啟偏向鎖的情況,列印資料如下

17:24:05 [t1] c.MyClass03 - synchronized前

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

17:24:05 [t1] c.MyClass03 - synchronized中

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001110 00000111 01001000 00000101

17:24:05 [t1] c.MyClass03 - synchronized后

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001110 00000111 01001000 00000101

使用了偏向鎖,并記錄了執行緒的值(101前面的一串數字),但是處于偏向鎖的物件解鎖后,執行緒id仍存盤于物件頭中,

關閉偏向鎖的情況,列印資料如下

17:28:24 [t1] c.MyClass03 - synchronized前

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

17:28:24 [t1] c.MyClass03 - synchronized中

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01110000 00100100 10101001 01100000

17:28:24 [t1] c.MyClass03 - synchronized后

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

使用輕量鎖(最后為000),并且記錄了占中存盤的鎖資訊地址(000前面一串數字),同步塊結束后恢復到原先狀態(因為沒有使用hashcode,所以hashcode值為0),

偏向鎖撤銷

在真正講解偏向撤銷之前,需要和大家明確一個概念——偏向鎖撤銷和偏向鎖釋放是兩碼事,

?撤銷:籠統的說就是多個執行緒競爭導致不能再使用偏向模式的時候,主要是告知這個鎖物件不能再用偏向模式

?釋放:和你的常規理解一樣,對應的就是 synchronized 方法的退出或 synchronized 塊的結束

何為偏向撤銷?

從偏向狀態撤回原有的狀態,也就是將 MarkWord 的第 3 位(是否偏向撤銷)的值,從 1 變回 0

如果只是一個執行緒獲取鎖,再加上「偏心」的機制,是沒有理由撤銷偏向的,所以偏向的撤銷只能發生在有競爭的情況下

撤銷-hashcode呼叫

呼叫了物件的hashcode會導致偏向鎖被撤銷:

?輕量級鎖會在鎖記錄中記錄hashcode

?重量級鎖會在Monitor中記錄hashcode

測驗代碼如下

private static Object object = new Object();
public synchronized static void main(String[] args){
    object.hashCode();//呼叫hashcode
    new Thread(()->{
        log.info("{}", "synchronized前");
        log.info("{}", toSimplePrintable(object));
        synchronized (object){
            log.info("{}", "synchronized中");
            log.info("{}", toSimplePrintable(object));
        }
        log.info("{}", "synchronized后");
        log.info("{}", toSimplePrintable(object));
    },"t1").start();
}

列印如下:

17:36:05 [t1] c.MyClass03 - synchronized前

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 01011111 00100001 00001000 10110101 00000001

17:36:06 [t1] c.MyClass03 - synchronized中

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01101110 00010011 11101001 01100000

17:36:06 [t1] c.MyClass03 - synchronized后

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 01011111 00100001 00001000 10110101 00000001

撤銷-其它執行緒使用物件

當有其它執行緒使用偏向鎖物件時,會將偏向鎖升級為輕量級鎖,

測驗代碼如下

private static void test2() {
    Thread t1 = new Thread(() -> {
        synchronized (object) {
            log.info("{}", toSimplePrintable(object));
        }
        synchronized (MyClass03.class) {
            MyClass03.class.notify();//t1執行完之后才通知t2執行
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        synchronized (MyClass03.class) {
            try {
                MyClass03.class.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.info("{}", toSimplePrintable(object));
        synchronized (object) {
            log.info("{}", toSimplePrintable(object));
        }
        log.info("{}", toSimplePrintable(object));
    }, "t2");
    t2.start();
}

列印資料如下

17:51:38 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01000111 00000000 11101000 00000101

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000001 01000111 00000000 11101000 00000101

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000001 01111000 00100000 01101001 01010000

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

可以看到執行緒t1是使用偏向鎖,執行緒t2使用鎖之前是一樣的,但是一旦使用了鎖,便升級為輕量級鎖,執行完同步代碼之后,恢復成撤銷偏向鎖的狀態,

撤銷-呼叫wait/notify

代碼如下

private static void test3(){
    Thread t1 = new Thread(() -> {
        log.info("{}", toSimplePrintable(object));
        synchronized (object) {
            log.info("{}", toSimplePrintable(object));
            try {
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("{}", toSimplePrintable(object));
        }
    }, "t1");
    t1.start();
    new Thread(() -> {
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (object) {
            log.debug("notify");
            object.notify();
        }
    }, "t2").start();
}

列印資料如下

17:57:57 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

17:57:57 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001111 00001100 11010000 00000101

17:58:02 [t2] c.MyClass03 - notify

17:58:02 [t1] c.MyClass03 - 00000000 00000000 01100000 00000000 00000011 11000001 10000010 01110010

呼叫wait和notify得是用Monitor,所以會從偏向鎖升級為重量級鎖,

批量重偏向

如果物件雖然被多個執行緒訪問,但沒有競爭,這是偏向了執行緒t1的物件仍然有機會重新偏向t2,重偏向會重置物件的Thread ID,

當撤銷偏向鎖閾值超過20次后,JVM會這樣覺得,我是不是偏向錯了呢,于是會在給這些物件加鎖時重新偏向至加鎖執行緒,

代碼如下

public static class Dog{}

private static void test4() {
    Vector<Dog> list = new Vector<>();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 30; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.info("{}", i+"\t"+toSimplePrintable(d));
            }
        }
        synchronized (list) {
            list.notify();
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        synchronized (list) {
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("===============> ");
        for (int i = 0; i < 30; i++) {
            Dog d = list.get(i);
            log.info("{}", i+"\t"+toSimplePrintable(d));
            synchronized (d) {
                log.info("{}", i+"\t"+toSimplePrintable(d));
            }
            log.info("{}", i+"\t"+toSimplePrintable(d));
        }
    }, "t2");
    t2.start();
}

列印如下

圖12

另外我在測驗的是否發現一個執行緒,當物件是普通類(如Dog)時,重偏向的閾值就是20,也就是第21次開啟了偏向鎖,但是如果把普通類替換成Object時,重偏向的閾值就是9,也就是第10次開啟了偏向鎖并重偏向(如圖13),這是怎么回事兒,有了解的同學可以評論交流下,

圖13

批量撤銷

當撤銷偏向鎖閾值超過40次后,JVM會這樣覺得,自己確實偏向錯了,根本不該偏向,于是整個類的所有物件都會變為不可偏向的,新建的物件也是不可偏向的,

代碼如下

static Thread t1, t2, t3;

private static void test6() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    int loopNumber = 40;
    t1 = new Thread(() -> {
        for (int i = 0; i < loopNumber; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.info("{}", i + "\t" + toSimplePrintable(d));
            }
        }
        LockSupport.unpark(t2);
    }, "t1");
    t1.start();
    t2 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.info("{}", i + "\t" + toSimplePrintable(d));
            synchronized (d) {
                log.info("{}", i + "\t" + toSimplePrintable(d));
            }
            log.info("{}", i + "\t" + toSimplePrintable(d));
        }
        LockSupport.unpark(t3);
    }, "t2");
    t2.start();
    t3 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.info("{}", i + "\t" + toSimplePrintable(d));
            synchronized (d) {
                log.info("{}", i + "\t" + toSimplePrintable(d));
            }
            log.info("{}", i + "\t" + toSimplePrintable(d));
        }
    }, "t3");
    t3.start();
    t3.join();
    log.info("{}", toSimplePrintable(new Dog()));
}

列印如下

圖14

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/543848.html

標籤:Java

上一篇:程式員健康最佳作息表,建議收藏!!

下一篇:Java-類加載器

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more