加锁方式
vim synchronizedTest.java
编写代码
class synchronizedTest {
public static void main(String args[]) {
}
public void a() {
synchronized(synchronizedTest.class) {
System.out.println("this is a");
}
}
public synchronized void b() {
System.out.println("this is b");
}
public static synchronized void c() {
System.out.println("this is c");
}
public void d() {
Object obj = new Object();
synchronized(obj) {
System.out.println("this is d");
}
}
}
编译查看字节码
javac synchronizedTest.java
javap -v synchronizedTest.class
Classfile /C:/JAVA/synchronized/synchronizedTest.class
Last modified 2022年9月5日; size 860 bytes
MD5 checksum c9e99f4a391cec22646502d70b90e3a6
Compiled from "synchronizedTest.java"
class synchronizedTest
minor version: 0
major version: 55
flags: (0x0020) ACC_SUPER
this_class: #2 // synchronizedTest
super_class: #7 // java/lang/Object
interfaces: 0, fields: 0, methods: 6, attributes: 1
Constant pool:
#1 = Methodref #7.#23 // java/lang/Object."<init>":()V
#2 = Class #24 // synchronizedTest
#3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #27 // this is a
#5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = String #30 // this is b
#7 = Class #31 // java/lang/Object
#8 = String #32 // this is c
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 a
#16 = Utf8 StackMapTable
#17 = Class #33 // java/lang/Throwable
#18 = Utf8 b
#19 = Utf8 c
#20 = Utf8 d
#21 = Utf8 SourceFile
#22 = Utf8 synchronizedTest.java
#23 = NameAndType #9:#10 // "<init>":()V
#24 = Utf8 synchronizedTest
#25 = Class #34 // java/lang/System
#26 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#27 = Utf8 this is a
#28 = Class #37 // java/io/PrintStream
#29 = NameAndType #38:#39 // println:(Ljava/lang/String;)V
#30 = Utf8 this is b
#31 = Utf8 java/lang/Object
#32 = Utf8 this is c
#33 = Utf8 java/lang/Throwable
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (Ljava/lang/String;)V
{
synchronizedTest();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 3: 0
public void a();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class synchronizedTest
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String this is a
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: goto 23
18: astore_2
19: aload_1
20: monitorexit
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
LineNumberTable:
line 6: 0
line 7: 5
line 8: 13
line 9: 23
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class synchronizedTest, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void b();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String this is b
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 12: 0
line 13: 8
public static synchronized void c();
descriptor: ()V
flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String this is c
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 16: 0
line 17: 8
public void d();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: new #7 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #8 // String this is d
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: aload_2
21: monitorexit
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
LineNumberTable:
line 20: 0
line 21: 8
line 22: 12
line 23: 20
line 24: 30
StackMapTable: number_of_entries = 2 ~ frame_type = 255 /* full_frame */ offset_delta = 25 locals = [ class synchronizedTest, class java/lang/Object, class java/lang/Object ] ~ stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 全部
}
可以发现:
a()
和d()
的策略是一样的,都具有monitorenter和monitorexit的操作。
b()
和c()
的策略是一样的,都在flags
字段中添加了ACC_SYNCHRONIZED
标记。
总结:
对于代码块加锁,使用的是monitor,而对于方法上修饰synchronized加锁,使用的是在flags中标记。
加锁对象
实例方法 | 对象实例 |
---|---|
静态方法 | Class实例 |
代码块 | 传入synchronized的对象实例 |
JDK1.6优化
在 HotSpot 虚拟机中,对象在内存中的布局分为三块区域:对象头,实例数据和对齐填充。
重点关注对象头(MarkWord + Class Metadada Address + Array Length)中的MarkWord。
Mark Word 用于存储对象自身的运行时数据,如 HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等等。占用内存大小与虚拟机位长一致(32位JVM -> MarkWord是32位,64位JVM -> MarkWord是64位)。
无锁
25bit | 4bit | 1bit(是否是偏向锁) | 2bit(锁标志位) |
---|---|---|---|
对象的hashCode | 对象分代年龄 | 0 | 01 |
偏向锁
23bit | 2bit | 4bit | 1bit | 2bit |
---|---|---|---|---|
线程ID | epoch | 对象分代年龄 | 1 | 01 |
偏向锁的加锁
- 偏向锁标志是未偏向状态,使用 CAS 将 MarkWord 中的线程ID设置为自己的线程ID,
- 如果成功,则获取偏向锁成功。
- 如果失败,则进行锁升级。
- 偏向锁标志是已偏向状态
- MarkWord 中的线程 ID 是自己的线程 ID,成功获取锁
- MarkWord 中的线程 ID 不是自己的线程 ID,需要进行锁升级
偏向锁的锁升级需要进行偏向锁的撤销。
偏向锁的撤销
- 对象是不可偏向状态
- 不需要撤销
- 对象是可偏向状态
- MarkWord 中指向的线程不存活
- 允许重偏向:退回到可偏向但未偏向的状态
- 不允许重偏向:变为无锁状态
- MarkWord 中的线程存活
- 线程ID指向的线程仍然拥有锁
- 升级为轻量级锁,将 mark word 复制到线程栈中
- 不再拥有锁
- 允许重偏向:退回到可偏向但未偏向的状态
- 不允许重偏向:变为无锁状态
- 线程ID指向的线程仍然拥有锁
- MarkWord 中指向的线程不存活
偏向锁指的就是JVM会认为只有某个线程才会执行同步代码(没有竞争的环境)。MarkWord会直接记录线程ID,只要线程来执行代码了,会比对线程ID是否相等,相等则当前线程能直接获取得到锁,执行同步代码。如果不相等,则用CAS来尝试修改当前的线程ID,如果CAS修改成功,那还是能获取得到锁,执行同步代码。如果CAS失败了,说明有竞争环境,此时会对偏向锁撤销,升级为轻量级锁。
轻量级锁
30bit | 2bit |
---|---|
指向线程栈锁记录的指针 | 00 |
轻量级锁状态下,当前线程会在栈帧下创建LockRecord,LockRecord 会把Mark Word的信息拷⻉进去,且有个Owner指针指向加锁的对象。线程执行到同步代码时,则用CAS试图将Mark Word的指向到线程栈帧的Lock Record,假设CAS修改成功,则获取得到轻量级锁;假设修改失败,则自旋(重试),自旋一定次数后,则升级为重量级锁。
重量级锁
30bit | 2bit |
---|---|
指向锁监视器的指针 | 10 |
线程进入同步代码块/方法时,monitor对象就会把当前进入线程Id进行存储,设置MarkWord的monitor对象地址,并把阻塞的线程存储到monitor的等待线程队列中,它加锁是依赖底层操作系统的 mutex 相关指令实现,所以会有用户态和内核态之间的切换,性能损耗十分明显。
mutex初始为0,表示锁可获取
加锁:monitorenter->计数器是否为0->计数器+1
解锁:monitorexit->是否为锁拥有者->计数器-1
为什么要采用锁升级机制
为什么切换用户态和内核态开销那么大
当程序中有系统调用语句,程序执行到系统调用时,首先使用类似int 80H
的软中断指令,保存现场,去的系统调用号,在内核态执行,然后恢复现场,每个进程都会有两个栈,一个内核态栈和一个用户态栈。当执行int中断执行时就会由用户态,栈转向内核栈。系统调用时需要进行栈的切换。而且内核代码对用户不信任,需要进行额外的检查。系统调用的返回过程有很多额外工作,比如检查是否需要调度等。
锁升级的意义
最根本的意义就是提升性能。
偏向锁和轻量级锁的设计理念就在于,期望短期只有较少的线程占有锁,占有锁的线程执行较短的时间便可以释放锁,因此可以使用CAS进行锁的替换,偏向锁和轻量级锁的主要性能开销就在于CAS自旋所耗费的CPU性能,短时间来说,这个性能消耗远小于用户态和内核态的切换。
但出现线程需要长时间的执行时,与其让等待的线程一直CAS长时间消耗CPU性能,不如切换用户态和内核态使用mutex加锁,让等待的线程进入挂起状态,等当前线程执行完毕再唤醒等待的线程。