2.1 定義java
Java Virtual Machine Stacks
(Java虛擬機棧)linux
垃圾回收是否涉及棧內存?shell
不涉及。棧內存無非就是一次次的方法調用產生的棧幀內存,棧幀內存在每一次方法調用後都會被彈出棧,也就是這部份內存會被自動的回收掉,因此並不須要垃圾回收來回收棧內存。windows
棧內存分配越大越好嗎?數組
不是。安全
-Xss size
。不指定的話,除了windows系統,默認都是1M,windows系統是依據虛擬內存大小分配。方法內的局部變量是否線程安全?多線程
變量是不是線程安全的,取決於這個變量被多線程共享時,每次運行結果和單線程運行的結果是不是同樣的。app
示例代碼1jvm
static void m1() { int x=0; for(int i=0;i<500;i++){ x++; } System.out.println(x); }
x
這個局部變量是線程安全的。每一個線程對應一個棧,而後線程內每次方法調用都會產生一個新的棧幀,因此x
變量處於不一樣線程的棧的棧幀中,互不影響,也就是線程安全的。工具
示例代碼2
public static void m1() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static void m2(StringBuilder sb) { sb.append(1); sb.append(2); sb.append(3); System.out.println(sb.toString()); } public static StringBuilder m3() { StringBuilder sb = new StringBuilder(); sb.append(1); sb.append(2); sb.append(3); return sb; }
m1
方法中sb
是線程安全的,由於其是線程內的局部變量。m2
方法中sb
是線程不安全的,由於其是做爲方法的參數傳遞進來,那麼就有可能有其餘線程可以訪問到這個變量,那麼這個變量就是多個線程共享的,可能形成值不一致,也就是線程不安全的。m3
方法中sb
是線程不安全的,雖然其是線程中的局部變量,可是其做爲返回值返回了,那麼就有可能被其餘線程使用,也就是多線程共享,可能形成值不一致,線程不安全。2.2 棧幀
棧幀存在於 Java 虛擬機棧中,是 Java 虛擬機棧中的單位元素,每一個線程中調用同一個方法或者不一樣的方法,都會建立不一樣的棧幀(能夠簡單理解爲,一個線程調用一個方法建立一個棧幀),因此,調用的方法鏈越多,建立的棧幀越多(例如:遞歸)。每調用一個新的方法,被調用方法對應的棧幀就會被放到棧頂(入棧),也就是成爲新的當前棧幀。當一個方法執行完成退出的時候,此方法對應的棧幀也相應銷燬(出棧)。
2.2.1 局部變量表(Local
2.2.1 局部變量表(Local Variable Table)
每一個棧幀中都包含一組稱爲局部變量表的變量列表,用於存放方法參數和方法內部定義的局部變量。在 Java 程序編譯成 Class 文件時,在 Class 文件格式屬性表中 Code 屬性的 max_locals(局部變量表所需的存儲空間,單位是 Slot) 數據項中肯定了須要分配的局部變量表的最大容量。
局部變量表的容量以變量槽(Variable Slot)爲最小單位,不過 Java 虛擬機規範中並無明確規定每一個 Slot 所佔據的內存空間大小,只是有導向性地說明每一個 Slot 都應該存放的8種類型: byte、short、int、float、char、boolean、reference(對象引用就是存到這個棧幀中的局部變量表裏的,這裏的引用指的是局部變量的對象引用,而不是成員變量的引用。成員變量的對象引用是存儲在 Java 堆(Heap)中)、returnAddress(虛擬機數據類型,returnAddress 類型的值就是指向特定指令內存地址的指針,JVM支持多線程,每一個線程有本身的程序計數器(pc register),而 pc 中的值就是當前指令所在的內存地址,即 returnAddress 類型的數據,當線程執行 native 方法時,pc 中的值爲 undefined)類型的數據,這8種類型的數據,均可以使用32位或者更小的空間去存儲。Java 虛擬機規範容許 Slot 的長度能夠隨着處理器、操做系統或者虛擬機的不一樣而發生變化。對於64位的數據類型,虛擬機會以高位在前的方式爲其分配兩個連續的 Slot 空間。即 long 和 double 兩種類型。作法是將 long 和 double 類型速寫分割爲32位讀寫的作法。不過因爲局部變量表創建在線程的堆棧上,是線程的私有數據,不管讀寫兩個連續的 Slot 是不是原子操做,都不會引發數據安全問題。
Java 虛擬機經過索引定位的方式使用局部變量表,索引值的範圍是從0開始到局部變量表最大的 Slot 數量。若是是32位數據類型的數據,索引 n 就表示使用第 n 個 Slot,若是是64位數據類型的變量,則說明要使用第 n 和第 n+1 兩個 Slot。
在方法執行過程當中,Java 虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程。若是是實例方法(非 static
方法),那麼局部變量表中的第0位索引的 Slot 默認是用來傳遞方法所屬對象實例的引用,在方法中能夠經過關鍵字 this
來訪問這個隱含的參數。其他參數按照參數表的順序來排列,佔用從1開始的局部變量 Slot,參數表分配完畢後,再根據方法體內部定義的變量順序和做用域分配其他的 Slot。
局部變量表中的 Slot 是可重用的,方法體中定義的變量,其做用域並不必定會覆蓋整個方法體,若是當前字節碼程序計數器的值已經超過了某個變量的做用域,那麼這個變量相應的 Slot 就能夠交給其餘變量去使用,節省棧空間,但也有可能會影響到系統的垃圾收集行爲。
局部變量無初始值(實例變量和類變量都會被賦予初始值),類變量有兩次賦初始值的過程,一次在準備階段,賦予系統初始值;另一次在初始化階段,賦予開發者定義的值。所以即便在初始化階段開發者沒有爲類變量賦值也沒有關係,類變量仍然具備一個肯定的默認值。但局部變量就不同了,若是一個局部變量定義了但沒有賦初始值是不能使用的。
使用一段代碼說明一下局部變量表:
// java 代碼 public int test() { int x = 0; int y = 1; return x + y; } // javac 編譯後的字節碼,使用 javap -v 查看 public int test(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iconst_1 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: ireturn LineNumberTable: line 7: 0 line 8: 2 line 9: 4 LocalVariableTable: Start Length Slot Name Signature 0 8 0 this Lcom/alibaba/uc/TestClass; 2 6 1 x I 4 4 2 y I
對應上面的解釋說明,經過 LocalVariableTable 也能夠看出來: Code 屬性: stack(int x(1個棧深度)+ int y(1個棧深度))=2, locals(this(1 Slot)+ int x(1 Slot)+ int y(1 Slot))=3, args_size(非 static 方法,this 隱含參數)=1
驗證 Slot 複用,運行如下代碼時,在 VM 參數中添加 -verbose:gc
:
public void test() { { byte[] placeholder = new byte[64 * 1024 * 1024]; } int a = 0; // 當這段代碼註釋掉時,System.gc() 執行後,也並不會回收這64MB內存。當這段代碼執行時,內存被回收了 System.gc(); }
局部變量表中的 Slot 是否還存在關於 placeholder 數組對象的引用。當 int a = 0;
不執行時,代碼雖然已經離開了 placeholder 的做用域,可是後續並無任何對局部變量表的讀寫操做,placeholder 本來所佔用的 Slot 尚未被其餘變量所複用,因此 placeholder 做爲 GC Roots(全部 Java 線程當前活躍的棧幀裏指向 Java 堆裏的對象的引用) 仍然是可達對象。當 int a = 0;
執行時,placeholder 的 Slot 被變量 a 複用,因此 GC 觸發時,placeholder 變成了不可達對象,便可被 GC 回收。
2.2.2 操做數棧(Operand Stack)
操做數棧是一個後入先出(Last In First Out)棧,方法的執行操做在操做數棧中完成,每個字節碼指令往操做數棧進行寫入和提取的過程,就是入棧和出棧的過程。
同局部變量表同樣,操做數棧的最大深度也是Java 程序編譯成 Class 文件時被寫入到 Class 文件格式屬性表的 Code 屬性的 max_stacks 數據項中。
操做數棧的每個元素能夠是任意的 Java 數據類型,32位數據類型所佔的棧容量爲1,64位數據類型所佔的棧容量爲2,在方法執行的任什麼時候候,操做數棧的深度都不會超過在 max_stacks 數據項中設定的最大值(指的是進入操做數棧的 「同一批操做」 的數據類型的棧容量的和)。
當一個方法剛剛執行的時候,這個方法的操做數棧是空的,在方法執行的過程當中,經過一些字節碼指令從局部變量表或者對象實例字段中複製常量或者變量值到操做數棧中,也提供一些指令向操做數棧中寫入和提取值,及結果入棧,也用於存放調用方法須要的參數及接受方法返回的結果。例如,整數加法的字節碼指令 iadd
(使用 iadd
指令時,相加的兩個元素也必須是 int 型) 在運行的時候將操做數棧中最接近棧頂的兩個 int 數值元素出棧相加,而後將相加結果入棧。
2.2.3 動態鏈接(Dynamic Linking)
每一個棧幀都包含一個指向運行時常量池(JVM 運行時數據區域)中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態連接。
在 Class 文件格式的常量池(存儲字面量和符號引用)中存有大量的符號引用(1.類的全限定名,2.字段名和描述符,3.方法名和描述符),字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分會在類加載過程的解析階段的時候轉化爲直接引用(指向目標的指針、相對偏移量或者是一個可以直接定位到目標的句柄),這種轉化稱爲靜態解析。另一部分將在每一次的運行期期間轉化爲直接引用,這部分稱爲動態鏈接。
看看如下代碼的 Class 文件格式的常量池:
// java 代碼 public Test test() { return new Test(); } // 字節碼指令 // Class文件的常量池 Constant pool: #1 = Methodref #4.#19 // java/lang/Object."<init>":()V #2 = Fieldref #3.#20 // com/alibaba/uc/Test.i:I #3 = Class #21 // com/alibaba/uc/Test #4 = Class #22 // java/lang/Object #5 = Utf8 i #6 = Utf8 I #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lcom/alibaba/uc/Test; #14 = Utf8 test #15 = Utf8 ()I #16 = Utf8 <clinit> #17 = Utf8 SourceFile #18 = Utf8 Test.java #19 = NameAndType #7:#8 // "<init>":()V #20 = NameAndType #5:#6 // i:I #21 = Utf8 com/alibaba/uc/Test #22 = Utf8 java/lang/Object public int test(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: getstatic #2 // Field i:I 3: areturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 4 0 this Lcom/alibaba/uc/Test;
從上面字節碼指令看出 0: getstatic #2 // Field i:I
這行字節碼指令指向 Constant pool 中的 #2,而 #2 中指向了 #3 和 #20 爲符號引用,在類加載過程的解析階段會被轉化爲直接引用(指向方法區的指針)。
2.2.4 方法返回地址
areturn
),這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱爲調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口(Normal Method Invocation Completion)。athrow
字節碼指令產生的異常,只要在本方法的異常處理器表中沒有搜索到匹配的異常處理器,就會致使方法退出,這種退出方法的方式稱爲異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的上層調用者產生任何返回值的。2.3 棧內存溢出
棧幀過多致使棧內存溢出
例如遞歸方法,當方法調用層級過多,產生大量的棧幀,卻沒有出棧,就會致使棧內存溢出,拋出StackOverflowError異常
示例代碼
public class Demo { private static int count; public static void main(String[] args) { try { method1(); } catch (Throwable e) { e.printStackTrace(); System.out.println(count); } } private static void method1() { count++; method1(); } }
執行結果
java.lang.StackOverflowError at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) at com.esell.Demo.method1(Demo.java:22) 14602
能夠看到方法總共執行了14602
次就致使了棧溢出,能夠在虛擬機運行參數中設置-Xss128k
調整棧內存大小,結果執行次數就會變小
若是 Java 虛擬機棧能夠動態擴展,而且在嘗試擴展的時候沒法申請到足夠的內存,或者在建立新的線程時沒有足夠的內存去建立對應的虛擬機棧,那 Java 虛擬機將拋出一個 OutOfMemoryError 異常
2.4 線程運行診斷
2.4.1 cpu佔用太高
示例代碼
public class Demo { public static void main(String[] args) { new Thread(null, () -> { System.out.println("1..."); while(true) { } }, "thread1").start(); new Thread(null, () -> { System.out.println("2..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread2").start(); new Thread(null, () -> { System.out.println("3..."); try { Thread.sleep(1000000L); } catch (InterruptedException e) { e.printStackTrace(); } }, "thread3").start(); } }
linux上運行此代碼
javac Demo.java nohup java Demo &
查看輸出
tail -f nohup.out
1... 2... 3... 1... 2... 3...
查看cpu情況
top
能夠看到剛纔運行的java程序佔用cpu很高,進程號爲10526
查詢該進程下全部線程的運行狀態
top -Hp 10526
能夠看到佔用cpu最高的線程是10536
使用jstack工具獲取10526進程中全部線程運行信息
jstack 10526
2019-12-17 17:24:00 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.11-b03 mixed mode): "Attach Listener" #12 daemon prio=9 os_prio=0 tid=0x00007f94d0001000 nid=0x294a waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" #11 prio=5 os_prio=0 tid=0x00007f94f8008800 nid=0x291f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "thread3" #10 prio=5 os_prio=0 tid=0x00007f94f80e7000 nid=0x292a waiting on condition [0x00007f94fd9e3000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at Demo.lambda$main$2(Demo.java:24) at Demo$$Lambda$3/1523554304.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "thread2" #9 prio=5 os_prio=0 tid=0x00007f94f80e5000 nid=0x2929 waiting on condition [0x00007f94fdae4000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at Demo.lambda$main$1(Demo.java:15) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "thread1" #8 prio=5 os_prio=0 tid=0x00007f94f80e3800 nid=0x2928 runnable [0x00007f94fdbe5000] java.lang.Thread.State: RUNNABLE at Demo.lambda$main$0(Demo.java:6) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007f94f80a8800 nid=0x2926 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f94f80a5800 nid=0x2925 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f94f80a3000 nid=0x2924 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f94f80a1000 nid=0x2923 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f94f8071800 nid=0x2922 in Object.wait() [0x00007f94fe1eb000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:142) - locked <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:158) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) "Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f94f806d800 nid=0x2921 in Object.wait() [0x00007f94fe2ec000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3521030> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157) - locked <0x00000000e3521030> (a java.lang.ref.Reference$Lock) "VM Thread" os_prio=0 tid=0x00007f94f8068800 nid=0x2920 runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007f94f80ad800 nid=0x2927 waiting on condition JNI global references: 152
其中thread1
、thread2
、thread3
是咱們本身建立的線程,其餘都是jvm
的線程
獲取佔用cpu最高的線程號的十六進制
printf '%x\n' 10536
2928
匹配jstack
獲得的線程信息
"thread1" #8 prio=5 os_prio=0 tid=0x00007f94f80e3800 nid=0x2928 runnable [0x00007f94fdbe5000] java.lang.Thread.State: RUNNABLE at Demo.lambda$main$0(Demo.java:6) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745)
最後,匹配代碼的第6行,得出佔用cpu太高的緣由的在線程中無限循環執行致使。
2.4.2 程序執行很長時間沒有結果
示例代碼
public class Demo { static A a = new A(); static B b = new B(); public static void main(String[] args) throws InterruptedException { new Thread(()->{ synchronized (a) { try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (b) { System.out.println("我得到了 a 和 b"); } } }).start(); Thread.sleep(1000); new Thread(()->{ synchronized (b) { synchronized (a) { System.out.println("我得到了 a 和 b"); } } }).start(); } } class A { } class B { }
linux運行此代碼
javac Demo.java nohup java Demo &
進程號爲10633,查看輸出時,發現一直沒有反應
根據進程號獲取全部線程信息
jstack 10633
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.11-b03 mixed mode): "Attach Listener" #11 daemon prio=9 os_prio=0 tid=0x00007fc3a8001000 nid=0x299f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "DestroyJavaVM" #10 prio=5 os_prio=0 tid=0x00007fc3d0008800 nid=0x298a waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Thread-1" #9 prio=5 os_prio=0 tid=0x00007fc3d00dd800 nid=0x2994 waiting for monitor entry [0x00007fc3ad8d2000] java.lang.Thread.State: BLOCKED (on object monitor) at Demo.lambda$main$1(Demo.java:21) - waiting to lock <0x00000000e3539580> (a A) - locked <0x00000000e3539590> (a B) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0" #8 prio=5 os_prio=0 tid=0x00007fc3d00db800 nid=0x2993 waiting for monitor entry [0x00007fc3ad9d3000] java.lang.Thread.State: BLOCKED (on object monitor) at Demo.lambda$main$0(Demo.java:13) - waiting to lock <0x00000000e3539590> (a B) - locked <0x00000000e3539580> (a A) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Service Thread" #7 daemon prio=9 os_prio=0 tid=0x00007fc3d00a8800 nid=0x2991 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007fc3d00a5800 nid=0x2990 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007fc3d00a3000 nid=0x298f waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007fc3d00a1000 nid=0x298e runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007fc3d0071800 nid=0x298d in Object.wait() [0x00007fc3c05fc000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:142) - locked <0x00000000e3520e78> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:158) at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) "Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007fc3d006d800 nid=0x298c in Object.wait() [0x00007fc3c06fd000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000e3521030> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:157) - locked <0x00000000e3521030> (a java.lang.ref.Reference$Lock) "VM Thread" os_prio=0 tid=0x00007fc3d0068800 nid=0x298b runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007fc3d00ad800 nid=0x2992 waiting on condition JNI global references: 151 Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007fc3b4003778 (object 0x00000000e3539580, a A), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007fc3b40062c8 (object 0x00000000e3539590, a B), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at Demo.lambda$main$1(Demo.java:21) - waiting to lock <0x00000000e3539580> (a A) - locked <0x00000000e3539590> (a B) at Demo$$Lambda$2/1072591677.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) "Thread-0": at Demo.lambda$main$0(Demo.java:13) - waiting to lock <0x00000000e3539590> (a B) - locked <0x00000000e3539580> (a A) at Demo$$Lambda$1/640070680.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock.
Found one Java-level deadlock
能夠在底部看到這句話,意思是死鎖,根據後面具體信息可知,在代碼29行,Thread-1
鎖住對象b,在等待對象a的鎖,而在代碼13行,Thread-0
鎖住對象a,在等待對象b的鎖,從而形成死鎖,程序無反應。
歡迎關注公衆號,後續文章更新通知,一塊兒討論技術問題 。