深入了解Synchronized原理
第一章:并发编程中的三个问题
1.1 可见性
可见性(Visibility):是指一个线程对共享变量进行修改,另一个先立即得到修改后的最新值。
可见性演示
案例演示:一个线程根据boolean类型的标记flag, while循环,另一个线程改变这个flag变量的值,另一个线程并不会停止循环。
/**
* 案例演示:
* 一个线程对共享变量的修改,另一个线程不能立即得到最新值
*/
public class Test01Visibility {
// 多个线程都会访问的数据,我们称为线程的共享数据
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (run) {
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
run = false;
System.out.println("时间到,线程2设置为false");
});
t2.start();
}
}
1.2 原子性
原子性(Atomicity):在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。
// 多次运行会发现问题
public class CleanCode {
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
number++;
}
};
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
ts.add(t);
}
for (Thread t : ts) {
t.join();
}
System.out.println("number = " + number);
}
}
反汇编target中thread的CleanCode.calss文件
javap -v -p CleanCode.class 是一个 Java 反编译命令,下面来详细解释一下每个部分的含义:
javap:这是 Java 提供的一个用于反编译的工具命令。
-v:这个选项表示输出详细信息,包括常量池等更多的细节。
-p:表示显示所有成员(包括私有成员)的信息。
当执行这个命令时,它会对名为 CleanCode.class 的字节码文件进行反编译,并按照详细且包括私有成员的方式输出相关的信息。这对于深入了解类的结构、方法签名、字段等非常有用,例如可以查看方法的指令序列、常量池的内容等。比如,如果 CleanCode 类中有一个私有方法 private void secretMethod(),使用 -p 选项就能获取到关于这个方法的反编译信息。
(base) ➜ thread git:(master) ✗ javap -v -p CleanCode.class
Classfile /Users/fanzhen/Documents/java-base-review/target/classes/src/main/java/thread/CleanCode.class
Last modified 2024-8-5; size 2272 bytes
MD5 checksum b99cdb5d0d673bb80ce1131eb988c984
Compiled from "CleanCode.java"
public class src.main.java.thread.CleanCode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #23.#56 // java/lang/Object."<init>":()V
#2 = InvokeDynamic #0:#61 // #0:run:()Ljava/lang/Runnable;
#3 = Class #62 // java/util/ArrayList
#4 = Methodref #3.#56 // java/util/ArrayList."<init>":()V
#5 = Class #63 // java/lang/Thread
#6 = Methodref #5.#64 // java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
#7 = Methodref #5.#65 // java/lang/Thread.start:()V
#8 = InterfaceMethodref #66.#67 // java/util/List.add:(Ljava/lang/Object;)Z
#9 = InterfaceMethodref #66.#68 // java/util/List.iterator:()Ljava/util/Iterator;
#10 = InterfaceMethodref #69.#70 // java/util/Iterator.hasNext:()Z
#11 = InterfaceMethodref #69.#71 // java/util/Iterator.next:()Ljava/lang/Object;
#12 = Methodref #5.#72 // java/lang/Thread.join:()V
#13 = Fieldref #73.#74 // java/lang/System.out:Ljava/io/PrintStream;
#14 = Class #75 // java/lang/StringBuilder
#15 = Methodref #14.#56 // java/lang/StringBuilder."<init>":()V
#16 = String #76 // number =
#17 = Methodref #14.#77 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#18 = Fieldref #22.#78 // src/main/java/thread/CleanCode.number:I
#19 = Methodref #14.#79 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#20 = Methodref #14.#80 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#21 = Methodref #81.#82 // java/io/PrintStream.println:(Ljava/lang/String;)V
#22 = Class #83 // src/main/java/thread/CleanCode
#23 = Class #84 // java/lang/Object
#24 = Utf8 number
#25 = Utf8 I
#26 = Utf8 <init>
#27 = Utf8 ()V
#28 = Utf8 Code
#29 = Utf8 LineNumberTable
#30 = Utf8 LocalVariableTable
#31 = Utf8 this
#32 = Utf8 Lsrc/main/java/thread/CleanCode;
#33 = Utf8 main
#34 = Utf8 ([Ljava/lang/String;)V
#35 = Utf8 t
#36 = Utf8 Ljava/lang/Thread;
#37 = Utf8 i
#38 = Utf8 args
#39 = Utf8 [Ljava/lang/String;
#40 = Utf8 increment
#41 = Utf8 Ljava/lang/Runnable;
#42 = Utf8 ts
#43 = Utf8 Ljava/util/List;
#44 = Utf8 LocalVariableTypeTable
#45 = Utf8 Ljava/util/List<Ljava/lang/Thread;>;
#46 = Utf8 StackMapTable
#47 = Class #85 // java/lang/Runnable
#48 = Class #86 // java/util/List
#49 = Class #87 // java/util/Iterator
#50 = Utf8 Exceptions
#51 = Class #88 // java/lang/InterruptedException
#52 = Utf8 lambda$main$0
#53 = Utf8 <clinit>
#54 = Utf8 SourceFile
#55 = Utf8 CleanCode.java
#56 = NameAndType #26:#27 // "<init>":()V
#57 = Utf8 BootstrapMethods
#58 = MethodHandle #6:#89 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#59 = MethodType #27 // ()V
#60 = MethodHandle #6:#90 // invokestatic src/main/java/thread/CleanCode.lambda$main$0:()V
#61 = NameAndType #91:#92 // run:()Ljava/lang/Runnable;
#62 = Utf8 java/util/ArrayList
#63 = Utf8 java/lang/Thread
#64 = NameAndType #26:#93 // "<init>":(Ljava/lang/Runnable;)V
#65 = NameAndType #94:#27 // start:()V
#66 = Class #86 // java/util/List
#67 = NameAndType #95:#96 // add:(Ljava/lang/Object;)Z
#68 = NameAndType #97:#98 // iterator:()Ljava/util/Iterator;
#69 = Class #87 // java/util/Iterator
#70 = NameAndType #99:#100 // hasNext:()Z
#71 = NameAndType #101:#102 // next:()Ljava/lang/Object;
#72 = NameAndType #103:#27 // join:()V
#73 = Class #104 // java/lang/System
#74 = NameAndType #105:#106 // out:Ljava/io/PrintStream;
#75 = Utf8 java/lang/StringBuilder
#76 = Utf8 number =
#77 = NameAndType #107:#108 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#78 = NameAndType #24:#25 // number:I
#79 = NameAndType #107:#109 // append:(I)Ljava/lang/StringBuilder;
#80 = NameAndType #110:#111 // toString:()Ljava/lang/String;
#81 = Class #112 // java/io/PrintStream
#82 = NameAndType #113:#114 // println:(Ljava/lang/String;)V
#83 = Utf8 src/main/java/thread/CleanCode
#84 = Utf8 java/lang/Object
#85 = Utf8 java/lang/Runnable
#86 = Utf8 java/util/List
#87 = Utf8 java/util/Iterator
#88 = Utf8 java/lang/InterruptedException
#89 = Methodref #115.#116 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#90 = Methodref #22.#117 // src/main/java/thread/CleanCode.lambda$main$0:()V
#91 = Utf8 run
#92 = Utf8 ()Ljava/lang/Runnable;
#93 = Utf8 (Ljava/lang/Runnable;)V
#94 = Utf8 start
#95 = Utf8 add
#96 = Utf8 (Ljava/lang/Object;)Z
#97 = Utf8 iterator
#98 = Utf8 ()Ljava/util/Iterator;
#99 = Utf8 hasNext
#100 = Utf8 ()Z
#101 = Utf8 next
#102 = Utf8 ()Ljava/lang/Object;
#103 = Utf8 join
#104 = Utf8 java/lang/System
#105 = Utf8 out
#106 = Utf8 Ljava/io/PrintStream;
#107 = Utf8 append
#108 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#109 = Utf8 (I)Ljava/lang/StringBuilder;
#110 = Utf8 toString
#111 = Utf8 ()Ljava/lang/String;
#112 = Utf8 java/io/PrintStream
#113 = Utf8 println
#114 = Utf8 (Ljava/lang/String;)V
#115 = Class #118 // java/lang/invoke/LambdaMetafactory
#116 = NameAndType #119:#123 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#117 = NameAndType #52:#27 // lambda$main$0:()V
#118 = Utf8 java/lang/invoke/LambdaMetafactory
#119 = Utf8 metafactory
#120 = Class #125 // java/lang/invoke/MethodHandles$Lookup
#121 = Utf8 Lookup
#122 = Utf8 InnerClasses
#123 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#124 = Class #126 // java/lang/invoke/MethodHandles
#125 = Utf8 java/lang/invoke/MethodHandles$Lookup
#126 = Utf8 java/lang/invoke/MethodHandles
{
private static int number;
descriptor: I
flags: ACC_PRIVATE, ACC_STATIC
public src.main.java.thread.CleanCode();
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 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lsrc/main/java/thread/CleanCode;
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: new #3 // class java/util/ArrayList
9: dup
10: invokespecial #4 // Method java/util/ArrayList."<init>":()V
13: astore_2
14: iconst_0
15: istore_3
16: iload_3
17: iconst_5
18: if_icmpge 51
21: new #5 // class java/lang/Thread
24: dup
25: aload_1
26: invokespecial #6 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
29: astore 4
31: aload 4
33: invokevirtual #7 // Method java/lang/Thread.start:()V
36: aload_2
37: aload 4
39: invokeinterface #8, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
44: pop
45: iinc 3, 1
48: goto 16
51: aload_2
52: invokeinterface #9, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
57: astore_3
58: aload_3
59: invokeinterface #10, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
64: ifeq 86
67: aload_3
68: invokeinterface #11, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
73: checkcast #5 // class java/lang/Thread
76: astore 4
78: aload 4
80: invokevirtual #12 // Method java/lang/Thread.join:()V
83: goto 58
86: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
89: new #14 // class java/lang/StringBuilder
92: dup
93: invokespecial #15 // Method java/lang/StringBuilder."<init>":()V
96: ldc #16 // String number =
98: invokevirtual #17 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
101: getstatic #18 // Field number:I
104: invokevirtual #19 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
107: invokevirtual #20 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
110: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
113: return
LineNumberTable:
line 10: 0
line 15: 6
line 16: 14
line 17: 21
line 18: 31
line 19: 36
line 16: 45
line 21: 51
line 22: 78
line 23: 83
line 24: 86
line 25: 113
LocalVariableTable:
Start Length Slot Name Signature
31 14 4 t Ljava/lang/Thread;
16 35 3 i I
78 5 4 t Ljava/lang/Thread;
0 114 0 args [Ljava/lang/String;
6 108 1 increment Ljava/lang/Runnable;
14 100 2 ts Ljava/util/List;
LocalVariableTypeTable:
Start Length Slot Name Signature
14 100 2 ts Ljava/util/List<Ljava/lang/Thread;>;
StackMapTable: number_of_entries = 4
frame_type = 254 /* append */
offset_delta = 16
locals = [ class java/lang/Runnable, class java/util/List, int ]
frame_type = 250 /* chop */
offset_delta = 34
frame_type = 252 /* append */
offset_delta = 6
locals = [ class java/util/Iterator ]
frame_type = 250 /* chop */
offset_delta = 27
Exceptions:
throws java.lang.InterruptedException
private static void lambda$main$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=0
0: iconst_0
1: istore_0
2: iload_0
3: sipush 1000
6: if_icmpge 23
9: getstatic #18 // Field number:I
12: iconst_1
13: iadd
14: putstatic #18 // Field number:I
17: iinc 0, 1
20: goto 2
23: return
LineNumberTable:
line 11: 0
line 12: 9
line 11: 17
line 14: 23
LocalVariableTable:
Start Length Slot Name Signature
2 21 0 i I
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 2
locals = [ int ]
frame_type = 250 /* chop */
offset_delta = 20
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_0
1: putstatic #18 // Field number:I
4: return
LineNumberTable:
line 7: 0
}
SourceFile: "CleanCode.java"
InnerClasses:
public static final #121= #120 of #124; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #58 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#59 ()V
#60 invokestatic src/main/java/thread/CleanCode.lambda$main$0:()V
#59 ()V
number++ 对应下面的数据,可以看到number++是由4条语句组成的
1.3 有序性
有序性(Ordering):是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。
<!-- jcstress 核心包 -->
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-core</artifactId>
<version>0.3</version>
</dependency>
<!-- jcstress测试用例包 -->
<dependency>
<groupId>org.openjdk.jcstress</groupId>
<artifactId>jcstress-samples</artifactId>
<version>0.3</version>
</dependency>
@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") // 定义测试结果为可接受的情况,id为1或4
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "danger") // 定义测试结果为有趣但可接受的情况,id为0
@State // 表明该类的状态可以被JCStress测试框架使用
public class CleanCode {
int num = 0; // 初始化一个整型变量num,用于测试数据竞争
boolean ready = false; // 初始化一个布尔型变量ready,用于控制测试线程的执行
/**
* actor1方法是测试线程之一,根据ready的值来决定返回值
* @param r 结果对象,用于存放计算结果
*/
@Actor
public void actor1(I_Result r) {
if (ready) {
r.r1 = num + num; // 如果ready为true,则计算并赋值r1
} else {
r.r1 = 1; // 如果ready为false,则直接赋值r1为1
}
}
/**
* actor2方法是测试线程之二,负责修改全局变量num和ready的值
* @param r 结果对象,用于存放计算结果
*/
@Actor
public void actor2(I_Result r) {
num = 2; // 修改num的值为2
ready = true; // 将ready设置为true,通知actor1可以进行计算
}
}
(base) ➜ java-base-review git:(master) mvn clean install
(base) ➜ java-base-review git:(master) java -jar target/jcstress.jar
第二章:Java内存模型(JMM)
2.1 计算机结构简介
冯诺依曼,提出计算机由五大组成部分,输入设备,输出设备存储器,控制器,运算器。
CPU:中央处理器,是计算机的控制和运算的核心,我们的程序最终都会变成指令让CPU去执行,处理程序中的数据。
内存:我们的程序都是在内存中运行的,内存会保存程序运行时的数据,供CPU处理。
缓存:CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。最靠近CPU的缓存称为L1,然后依次是 L2,L3和主内存,CPU缓存模型如图下图所示。
CPU Cache分成了三个级别: L1, L2, L3。级别越小越接近CPU,速度也更快,同时也代表着容量越小。
- L1是最接近CPU的,它容量最小,例如32K,速度最快,每个核上都有一个L1 Cache。
- L2 Cache 更大一些,例如256K,速度要慢一些,一般情况下每个核上都有一个独立的L2 Cache。
- L3 Cache是三级缓存中最大的一级,例如12MB,同时也是缓存中最慢的一级,在同一个CPU插槽之间的核共享一个L3 Cache。
Cache的出现是为了解决CPU直接访问内存效率低下问题的,程序在运行的过程中,CPU接收到指令后,它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,如果命中缓存,CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写人,当运算结束之后,再将CPUCache中的最新数据刷新
到主内存当中,CPU通过直接访问Cache的方式替代直接访问主存的方式极大地提高了CPU 的吞吐能力。但是由于一级缓存(L1 Cache)容量较小,所以不可能每次都命中。这时CPU会继续向下一级的二级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3
Cache、内存(主存)和硬盘。
2.2 Java内存模型 Java Memory Molde
- 主内存
主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。 - 工作内存
每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。
Java内存模型的作用:Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。
第三章:synchronized保证三大特性
synchronized能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。
synchronized (锁对象) {
// 受保护资源;
}
3.1 synchronized保证原子性
public class Test01Atomicity {
private static int number = 0;
public static void main(String[] args) throws InterruptedException {
Runnable increment = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
synchronized (Test01Atomicity.class) {
number++;
}
}
}
};
ArrayList<Thread> ts = new ArrayList<>();
for (int i = 0; i < 50; i++) {
Thread t = new Thread(increment);
t.start();
ts.add(t);
}
for (Thread t : ts) {
t.join();
}
System.out.println("number = " + number);
}
}
3.2 synchronized保证可见性
案例演示:一个线程根据boolean类型的标记flag, while循环,另一个线程改变这个flag变量的值,另一个线程并不会停止循环。
/**
案例演示:
一个线程根据boolean类型的标记flag, while循环,另一个线程改变这个flag变量的值,
另一个线程并不会停止循环.
*/
public class Test01Visibility {
// 多个线程都会访问的数据,我们称为线程的共享数据
private static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (run) {
// 增加对象共享数据的打印,println是同步方法
// sout里面的方式是synchronized修饰的
System.out.println("run = " + run);
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
run = false;
System.out.println("时间到,线程2设置为false");
});
t2.start();
}
}
synchronized保证可见性的原理,执行synchronized时,会对应lock原子操作会刷新工作内存中共享变量的值。
主内存与工作内存之间的数据交互过程
lock -> read -> load -> use -> assign -> store -> write -> unlock
3.3 synchronized有序性
as-if-serial语义: 不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的。
synchronized保证有序性的原理,我们加synchronized后,依然会发生重排序,只不过,我们有同步代码块,可以保证只有一个线程执行同步代码中的代码。保证有序性。
4. synchronized不可中断特性
什么是不可中断:一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。
package thread;
public class Uninterruptible {
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
// 1.定义一个Runnable
Runnable run = () -> {
// 2.在Runnable定义同步代码块
synchronized (obj) {
String name = Thread.currentThread().getName();
System.out.println(name + "进入同步代码块");
// 保证不退出同步代码块
try {
Thread.sleep(888888);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 3.先开启一个线程来执行同步代码块
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
// 4.后开启一个线程来执行同步代码块(阻塞状态)
Thread t2 = new Thread(run);
t2.start();
// 5.停止第二个线程
System.out.println("停止线程前");
t2.interrupt();
System.out.println("停止线程后");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
结果:
Thread-0进入同步代码块
停止线程前
停止线程后
TIMED_WAITING
BLOCKED
synchronized属于不可被中断
Lock的lock方法是不可中断的
Lock的tryLock方法是可中断的
5. 通过javap学习synchronized原理
5.1 同步代码块
public class Demo01 {
private static Object obj = new Object();
public static void main(String[] args) {
synchronized (obj) {
System.out.println("1");
}
}
public synchronized void test() {
System.out.println("a");
}
}
javap -p -v class文件
synchronized进行加锁的时候会关联一个monitor,monitor才是真正的锁。如果没有monitor,JVM会创建一个monitor对象。
monitor里面有两个重要的成员变量一个是owner:拥有锁的线程。
另一个是recursions:记录获取锁的次数。
当一个线程t1走到synchronized同步代码块的时候,会先找到monitor,如果这个锁没人用,那么monitor的owner就会变成t1,且recursions变为1次,可以理解为(JVM规范中对于monitorenter的描述):
- 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)
- 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
- 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。
synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待。
monitorexit
- 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
- 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权.
synchronized 同步代码块中有异常会释放锁。
5.2 同步方法
同步方法在反汇编后,会增加 ACC_SYNCHRONIZED 修饰。会隐式调用monitorenter和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。
5.3 小结
通过javap反汇编我们看到synchronized使用编程了monitorentor和monitorexit两个指令。每个锁对象都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁的线程,recursions会保存线程获得锁的次数,当执行到monitorexit时,recursions会-1,当计数器减到0时这个线程就会释放锁。
Synchronized和Lock的区别
- synchronized是关键字,Lock是一个接口。
- synchronized会自动释放锁,Lock需要手动释放锁。
- synchronized是不可中断的,Lock可以终端也可以不中断。
- 通过Lock可以知道线程是否获取到锁了,而synchronized不可以。
- synchronized能锁住方法和代码块,Lock只能锁住代码块。
- Lock可以使用读锁提高多线程的读效率。
- synchronized是非公平锁,像ReentrantLock可以控制是否是公平锁。
6. 深入JVM源码monitor监视器锁
https://www.bilibili.com/video/BV1aJ411V763?p=15&vd_source=240d9002f7c7e3da63cd9a975639409a
// todo