重温 JAVA -- synchronized 终
文章目录
1、基本介绍
synchronized
关键字用于实现多线程之间的同步操作。synchronized
可用于修饰 类方法, 对象方法, 代码块
使用 syncronized
时,需要有监视器。当修饰 类方法时, 监视器为该类; 当修饰 对象方法, 监视器为该对象; 当修饰 代码块, 需要手动传入监视器
1.1、类方法
示例代码
public class SynchronizedTest {
public synchronized static void classMethod() {
}
public static void commonClassMethod() {
}
}
反编译
public static synchronized void classMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 12: 0
public static void commonClassMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 15: 0
从反编译角度来看,加了 synchronized
关键字的,flags
多了 ACC_SYNCHRONIZED
。
1.2、对象方法
示例代码
public synchronized void objMethod() {
}
public void commonObjMethod() {
}
}
反编译
public synchronized void objMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 12: 0
public void commonObjMethod();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 15: 0
同类方法,被 synchronized
修饰的对象方法,flags
多了 ACC_SYNCHRONIZED
标识
1.3、代码块
示例代码
public class SynchronizedTest {
public void codeBlock() {
synchronized ("monitor") {
System.out.println("codeBlock");
}
}
}
反编译
public void codeBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // String monitor
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String codeBlock
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 10: 0
line 11: 5
line 12: 13
line 13: 23
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 18
locals = [ class com/csp/boot/SynchronizedTest, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
可以看到在指令层面多加了 monitorenter
, monitorexit
。有一点需要注意:上面有 2个 monitorexit
为什么 1 个 synchronized 会产生 2 个
monitorexit
指令?
这是为了保证,方法在发生异常时,也能正常释放锁。
1.4、ACC_SYNCHRONIZED、monitorenter、monitorexit
monitorenter
每个对象都关联一个 monitor
,当线程执行 monitorenter
时,monitor
会被锁定;monitor
内部维护着一个被锁次数的计数器,对象未被锁定时,次数为0。线程执行 monitorenter
计数器会 +1。当同一个线程再次获得该对象的锁时,计数器会再加1。其他线程想获得该 monitor
时,就会阻塞,直到计数器为0才能成功。
monitorexit
只有持有 monitor
的线程,才能执行该指令。
每次执行 monitorexit
monitor
内部的计算器就会 -1。
ACC_SYNCHRONIZED
方法级别的锁是隐式的,当方法被 synchronized
标记时,在方法的常量池中会有 ACC_SYNCHRONIZED
标记。
一个方法在被调用时,需要先判断是否有 ACC_SYNCHRONIZED
标记;如果有则线程需要先持有 monitor
锁。当方法执行完毕时,会自动释放锁,不管方法执行成功,还是失败。
2、对象头
在对象的内存布局中,包含对象头,对象实例数据,对齐填充。其中,对象头的 mark word
记录了锁的信息。
mark word
3、锁升级
在早期,synchronized
是一个重量级锁,自从 jdk 1.6
版本之后,对 synchronized
锁进行了优化,引入了 偏向锁, 轻量锁。
3.1、偏向锁
偏向锁是一种加锁,解锁都非常快速的锁。
大部分情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,偏向锁因此被引入
3.1.1、偏向锁加锁
场景一:锁对象未被线程持有
线程发现锁是匿名偏向状态(threadId
未指向任何线程),会使用 CAS
将 mark word
中 threadId
指向本身。如果成功,则获取锁;否则加锁失败,升级为轻量锁。
场景二:获取偏向锁线程再次进入同步块
线程进入同步块时,发现 threadId
指向的就是自身。此时会往自身线程栈中添加一条 Displaced Mark Word
为空的 Lock Record
。因为操作的是线程的私有变量,因此无需 CAS
。
注:Displaced Mark Word 是 JVM 为线程在栈帧中创建用于存储锁记录的空间
从场景一,场景二 来看,偏向锁的加锁效率是非常高的。
对于偏向锁而言,只要发生锁竞争,那么会立马升级为轻量锁
3.1.2、偏向锁解锁
解锁过程
偏向锁线程,在解锁时,仅仅是将自身线程栈中最后一条 Lock Record
的 obj
设置为 null
。
目的
未去修改 Mark Word
中 threadId
,仅仅是为了同一个线程再次进入同步代码块时,无需使用 CAS
。(偏向锁被引入的原因是:锁竞争情况少,且总是由同一个线程多次获得)
偏向锁解锁效率非常高
3.1.3、撤销偏向锁
未获取偏向锁线程进入同步块
未获取偏向锁线程进入同步块,发现 threadId
指向的不是自己,且是偏向状态,那么此时会进入到 撤销偏向锁的流程中。一般会在 safepoint
时,查看偏向锁线程是否存活。
- 偏向锁线程死亡,或不在同步代码块中,则将
mark word
设置为无锁状态。 - 偏向锁线程未死亡,且在同步代码块中。则将锁升级为轻量级锁,原偏向锁线程,继续持有锁。偏向锁线程释放锁时,按照轻量级锁方式释放。其他线程则自旋尝试获取轻量锁。
为什么在撤销偏向锁时,需要查看偏向锁线程是否死亡,或者在不在同步代码块中
因为,偏向锁在解锁时,仅仅是修改了自身栈中的值。
3.2、轻量锁
3.2.1、轻量锁加锁
流程
先将 Mark Word
复制一份到 Displaced Mark Word
,然后用 CAS
将 Mark Word
替换为自己栈中锁记录的指针。如果成功,则持有锁;失败则,自旋继续尝试获取锁。
自旋获取锁
轻量锁,不会无限次的自旋获取锁,而是在自旋一定次数后(自适应),如果还是未获取到锁,则将锁升级为重量锁
3.2.2、轻量锁解锁
解锁则是用 CAS
将 Displaced Mark Word
复制回锁对象的 Mark Word
。如果成功则表示没有竞争。如果失败,表示当前锁存在竞争,锁会膨胀为重量级锁
3.3、重量锁
当多个线程同时访问重量锁时,重量锁会设置几种状态区分线程(线程在请求重量锁失败后,会进入阻塞状态)
Contention List: 所有请求锁的线程,在请求锁失败后,会被放入该集合中。该集合的数据结构是先进后出
Entry List: Contention List
会让有机会成为候选人的线程放入到此集合中,主要是为了减少对 Contention List
的并发访问。
OnDeck: 任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Wait Set: 线程调用 wait
之后会被放入该队列中
Owner: 获得锁的线程被称为 Owner
!Owner: 释放锁的线程
3.3.1、执行流程
步骤1:线程在进入 Contention List
之前,线程会 CAS
尝试获取锁,获取不到才会进入 Contention List
尾部
步骤2:Owner
线程在解锁时,如果 Entry List
为空,那么会优先将 Contention List
部分线程移入 Entry List
步骤3:Owner
线程在解锁时,如果 Entry List
为空,那么会从 Contention List
队尾取一个线程成为 OnDeck
。如果 Entry List
不为空,则从 Entry List
中选一个成为 OnDeck
。 OnDeck
需要与还未进入 Contention List
,还在自旋获取锁的线程进行竞争
步骤4:OnDeck
线程获得锁,成为 Owner
线程
步骤5:调用 wait
释放锁,进入 Wait Set
队列
步骤6:Onwer
线程执行 notify
或 notifyAll
,Wait Set
中的某个线程或所有线程被唤醒