锁的升级

锁的升级

Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,在 Java SE 1.6 中,锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

  • 偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。

  • 轻量级锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。

  • 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况。

不同级别锁的优缺点对比如下:

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

偏向锁

多数情况下,锁不会存在竞争,而是同一个线程多次获得。当某个线程访问同步块代码时,会将锁对象和栈帧中的锁记里存储锁偏向的线程 ID,以后线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单比对一下对象头中的 MarkWord 里的线程 ID,如果一致则表示线程获得锁。若不一致,再继续测试偏向锁的标识是否为 1:如果没有设置(无锁状态),用 CAS(Compare and Swap)竞争锁;如果设置了,尝试使用 CAS 将对象头的偏向锁指向当前线程。

当有另一个线程尝试竞争锁时,持有偏向锁的线程才会释放锁。需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态,如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 Mark Word,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

Java 6,7 默认开启偏向锁,可以通过 JVM 的参数 -XX:-UsebiasedLocking=false 关闭。

轻量级锁

加锁的流程中,锁记录存储在栈桢,会将对象头的 MarkWord 复制到锁记录。线程在执行同步块时,会尝试用 CAS 将对象头的 MarkWord 替换为指向锁记录的指针,若成功,获得锁;失败表示其他线程竞争锁,当前线程尝试使用自旋获取锁。解锁的流程中,类似于加锁反向操作,会将锁记录复制会对象头的 MarkWord。若成功,表示操作过程中没有竞争发生;若失败,存在竞争,锁会膨胀成重量级锁。

当膨胀到重量级锁时,不会再通过自选获得锁(自旋时线程处于活动状态,会消耗 CPU),而是将线程阻塞,获得锁的线程执行完后会释放重量级锁,此时唤醒因为锁阻塞的线程,进行新一轮的竞争。

重量级锁

Links

上一页