与 ReentrantLock 区别
ReentrantLock 独有能力
-
类库层面的同步
-
等待可中断,持有锁的线程长期不释放锁的时候,等待的线程可以放弃等待。
-
可实现公平锁 ,按照申请锁的时间顺序获取锁,不过公平锁讲导致其性能的急速下降,明显影响吞吐量。
-
锁绑定多个条件,一个 ReentrantLock 可以同时绑定多个 Condition 对象,而 synchronized 与 notifyAll 配合之恶能实现一个隐含的条件
为什么保留 synchronized
- synchronized 是 Java 语法层面的同步,简单清晰
- Lock 需要主动释放锁
- JVM 在线程和对象元数据中记录了 synchronized 锁的相关信息,而 Lock 没有
底层实现
- 当方法内部使用 synchronized ,monitorenter 尝试获取对象的锁,monitorexit 用于释放锁;
第二个 monitorexit 用于处理程序可能发生的异常,由编译器自动生成,在发生异常时处理异常然后释放掉锁。
- 当 synchronized 修饰方法块时,是根据 ACC_SYNCHRONIZED 标志位去控制同步逻辑
public class Test3 {
public void methodA(){
synchronized (Test3.class){
System.out.println(123);
}
}
public static void main(String[] args) {
}
}
对应字节码
public void methodA();
Code:
0: ldc #2 // class cn/com/egova/base/canal/handler/Test3
2: dup
3: astore_1
4: monitorenter
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: bipush 123
10: invokevirtual #4 // Method java/io/PrintStream.println:(I)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
对方法加锁时,会有 ACC_SYNCHRONIZED 标志
public class Test4 {
public synchronized void test(){
System.out.println(123);
}
public static void main(String[] args) {
}
}
public synchronized void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: bipush 123
5: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
8: return
LineNumberTable:
line 10: 0
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcn/com/egova/base/canal/handler/Test4;
锁优化
- 自旋锁与自适应自旋
Java 的线程时映射到操作系统的原生内核线程上,挂起线程和恢复线程需要转到内核态进行,比较消耗资源;所以 JVM 会让线程执行一个忙等待(自旋),然后再获取锁。
JDK 6 对自旋锁进行了优化,等待的时间根据同一个锁上面的自旋时间和锁的拥有者的状态来决定。
若上一次自旋刚获取成功过锁,那么 JVM 允许线程等待相对更长的时间;若自旋很少成功获得锁,那么会直接省略自旋的过程
- 锁消除
JIT 即时编译器通过逃逸分析技术,发现 synchronized 锁对象,只有一个线程能加锁,不存在共享数据竞争的问题,那么会将锁进行消除。
例如,下面代码在字节码层面会转换为 StringBuilder.append 方式,而 append 方法都有一个同步块,JVM 分析到参数只会在方法内部调用,不存在共享数据的竞争,这里的锁可以被消除掉。
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
- 锁粗化
若一系列的连续操作都是对同一个对象反复加锁和解锁,甚至加锁操作在循环体中,JVM 会将加锁同步的范围扩展(粗化)到操作外部。
- 轻量级锁
相对于 synchronized 重量级锁而言,它的设计初衷,就是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。
实现:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象 Mark Word 的拷贝;然后 JVM 通过 CAS 操作,将锁对象的 Mark Word 更新为指向 Lock Record 的指针,CAS 成功表示成功获取到锁,Mark Word 中的锁标志位也改为 01
若 CAS 更新失败,说明当前存在竞争,然后 JVM 检查锁对象的 Mark Word 是否指向当前线程的栈帧,如果是,那么说明线程已经拥有这个对象的锁,直接进入同步方法块;否则轻量级锁膨胀为重量级锁,锁标志位改为 10。
- 偏向锁
意思是锁会偏向于第一个获得它的线程,在后面执行过程中,锁没有被其它线程获取,那么持有偏向锁的线程将无需再进行同步。
偏向锁可以提高带有同步但无竞争的程序性能,但是对于程序中大多数锁总是被不同线程访问的情况,偏向锁就是多余的。
轻量级锁是在无竞争条件下使用 CAS 操作去消除同步带来的互斥量;偏向锁是在无竞争的情况下,把整个同步都消除,连 CAS 操作都不做。
锁升级
synchronized 锁升级过程: