理解 Java 关键字 synchronized

概述

synchronized 是实现互斥同步的一种方法。synchronized 被编译成字节码后,会在需要同步的代码块前后分别插入 monitorenter 和 monitorexit 指令,JVM 会保证每个 monitorenter 指令都有一个对应的 monitorexit 指令配对。这两个指令都需要一个 reference 类型的参数 objectref 作为锁。

synchronized 支持线程重入。当 JVM 执行字节码遇到 monitorenter 指令时,首先尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经拥有了该锁,那么会把锁的计数器加 1;相应的,当执行 monitorexit 指令时,会将锁的计数器减 1。当计数器为 0 时,则释放锁。当多个线程竞争同一个锁时,如果一个线程获得锁,那么其他线程就会阻塞等待,直到该锁被释放。由于 Java 的线程都是映射到操作系统的原生线程上,如果要阻塞或者唤醒一个线程,都需要从用户态切换到核心态。因此在 Java 6 之前,synchronized 都是一个比较“重”的操作;但在 Java 6 中,synchronized 得到了大量优化,提高了运行效率。

优化

Java6 加入了许多的锁优化措施,包括:

  • 适应性自旋(Adaptive Spinning)
  • 锁消除(Lock Elimination)
  • 锁粗化(Lock Coarsening)
  • 轻量级锁(Lightweight Locking)
  • 偏向锁(Biased Locking)

自旋锁与自适应自旋

在许多应用上,线程持有锁的时间都较短,因此没有必要将其他线程挂起。一个更好的方法是让线程执行一个忙循环(自旋),等待另一个线程释放锁。这就是所谓的自旋锁。

通常情况下,如果锁被占用的时间很短,那么自旋等待的效果就很非常好;反之,如果锁被占用的时间很长,那么自旋线程就会浪费处理器资源。所以,确定自旋时间是非常重要的。

自适应自旋可以根据上一次在同一个锁上的自旋时间和锁的拥有者的状态来确定下一次自旋时间。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能成功,那么就会允许自旋等待持续相对更长的时间。反之,如果对于某个锁,自旋等待很少成功,那么以后线程要获取这个锁时就可能减少自旋时间了。

锁消除

锁消除机制是指虚拟机消除那些不可能存在共享数据竞争的锁,主要使用逃逸分析技术做判断:在一段代码中,如果堆上的所有数据都不会逃逸出去被其它线程访问到,那么可以把它们当做栈上数据对待,认为它们是线程私有的,那么就不需要加锁了。

锁粗化

锁粗化是把同步块的作用范围扩大,避免对同一个对象反复加锁和解锁。

轻量级锁

轻量级锁是相对于使用操作系统互斥量实现的传统锁而言,因此传统的锁也称为“重量级”锁。轻量级锁不能代替重量级锁,其作用是在没有多线程竞争的情况下,减少重量级锁因挂起和恢复线程产生的性能消耗。

轻量级锁基于 CAS 操作:不考虑其他线程竞争锁,先使用 CAS 操作尝试获取锁。如果出现了竞争,那么这时候锁再膨胀成为重量级锁。

偏向锁

如果说轻量级锁是在无竞争情况下使用 CAS 操作消除同步使用的互斥量,那么偏向锁就是在无竞争的情况下把整个同步都消除掉了。

阻塞同步和非阻塞同步

synchronized 是阻塞同步的一种实现(Blocking Synchronization)。阻塞同步是一种悲观的并发策略,总是要进行加锁(不考虑虚拟机优化),完成用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

但是随着硬件指令集的发展,出现了非阻塞同步,一种基于冲突检测的乐观并发策略,即先进行操作,如果没有其他线程争夺共享数据,那么操作成功;如果有其它线程争夺数据,那么这时就有冲突,需要进行补偿(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略不需要将线程挂起,避免了用户态核心态的转换。ReentrantLock 便是基于非阻塞同步。

与 ReentrantLock 区别

ReentrantLock 与 synchronized 相似,都允许同一线程重入。ReentrantLock 基于 AQS,本质是乐观锁机制。除了同步机制不一样,两者不同之处还表现在,ReentrantLock 允许:

  • 等待可中断
  • 公平锁
  • 同一锁与多个 Condition 绑定

参考

  1. 深入理解 Java 虚拟机
  2. Java 并发编程的技术

Previous post: 理解 Java LockSupport 工具

Next post: 理解 Java volatile 关键字