synchronized 关键字

在 Java 中,synchronized 关键字用于实现多线程环境下的同步控制,确保只有一个线程可以访问共享资源或代码块,从而避免数据的不一致性和竞争条件。

Java 多线程的锁都是基于对象的,Java 中的每一个对象都可以作为一个锁。

有以下三种形式:

  1. 关键字在实例方法上,锁为当前实例
  2. 关键字在静态方法上,锁为当前 Class 对象
  3. 关键字在代码块上,锁为括号里面的对象hex

四种锁状态

一个对象其实有四种锁状态,它们级别由低到高依次是:

  1. 无锁状态:
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件会比较苛刻,锁降级发生在 Stop The World 期间,当 JVM 进入安全点的时候,会检查是否有闲置的锁,然后进行降级。

无锁(No Lock)

当代码块不涉及多线程竞争时,JVM 不会为其分配锁。没有对资源进行锁定,任何线程都可以尝试去修改它。

偏向锁(Biased Lock)

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

JVM 会将锁的状态标记为“偏向锁”并记录拥有锁的线程 ID。如果在偏向期间没有其他线程尝试获取该锁,那么持有偏向锁的线程就不需要进行任何同步操作。

锁撤销:如果在偏向锁期间,另一个线程尝试获取该锁,那么偏向锁会被撤销,并升级为轻量级锁(自旋锁)。

偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下:

  1. 在一个安全点(在这个时间点上没有字节码正在执行)停止拥有锁的线程。
  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和 Mark Word,使其变成无锁状态。
  3. 唤醒被停止的线程,将当前锁升级成轻量级锁。

轻量级锁(Lightweight Lock)

轻量级锁是一种适用于短时间内只涉及少量线程竞争的场景(或者多个线程在不同时段获取同一把锁)。此时 JVM 会使用自旋锁(spin lock)机制来避免线程阻塞和上下文切换的开销。

自旋锁的工作原理是:线程在尝试获取锁时,不会立即阻塞自己,而是自旋等待(即反复检查锁是否可用),以期锁可以很快被释放。在短时间内,锁的持有时间很短时,自旋锁的性能优于重量级锁,因为避免了线程的挂起和唤醒操作。

锁膨胀:如果线程在自旋一段时间后仍未获得锁,轻量级锁将升级为重量级锁。

重量级锁(Heavyweight Lock)

重量级锁使用操作系统的同步机制来实现。当锁升级为重量级锁时,JVM 会阻塞那些尝试获取锁但未能成功的线程。被阻塞的线程在获得锁之前必须进入等待状态,直到其他线程释放锁。

重量级锁会导致线程上下文切换和系统调用,这些操作通常比轻量级锁开销大。

锁的升级流程

  1. 无锁到偏向锁:对象初始化时,默认处于无锁状态。如果一个线程尝试获取锁,且该锁目前处于无锁状态,则升级为偏向锁。
  2. 偏向锁到轻量级锁:如果偏向锁被另一个线程竞争,偏向锁将被撤销,锁状态升级为轻量级锁。
  3. 轻量级锁到重量级锁:当自旋锁的线程数量超过一定阈值(或自旋时间过长),锁会从轻量级锁升级为重量级锁,线程被阻塞等待锁释放。

锁的对比

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁的竞争的线程使用自旋会消耗 CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗 CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行时间较长。