Java 锁优化

对象头和锁

对象头在 JVM: HotSpot虚拟机中的对象 对象的内存布局 中有提到过,用于保存对象的系统信息。

对象头中有一个称为 Mark Word 的部分,它是实现锁的关键。它是一个多功能的数据区,可以存放对象的 哈希值、对象年龄、锁的指针等信息。对象是否占用锁,以及占用哪个锁就记录在 Mark Word 中。

Mark Word 在32位系统中占用32位,在64位系统中占用64位。

普通对象的对象头(32位系统):
hash:25 ------>| age:4 biased_lock:1 lock:2
Mark Word中有 25位比特表示对象的哈希值,4位比特表示对象的年龄,1位比特表示是否为偏向锁,2位比特表示锁的信息。

JVM中锁的实现和优化

如果将所有的线程竞争都交由操作系统处理,那么并发性能是非常低下的。JVM 在操作系统层面挂起线程之前,会尽一切可能在虚拟机层面上解决竞争关系,避免真实的竞争发生。同时,在竞争不激烈的时候也会试图消除不必要的竞争。

JVM 的锁优化手段包括:偏向锁、轻量级锁、自旋锁、锁消除、锁膨胀等。

偏向锁

偏向锁是 JDK 1.6 提出的一种锁优化方式。

如果程序没有竞争,则取消之前已经取得锁的线程同步操作。也就是,若某一线程被线程获取后,便进入偏向模式,当线程再次请求这个锁时,无须再进行相关的同步操作,从而节省了操作时间。如果在此之前有其他线程进行了锁请求,则锁退出偏向模式。

当锁对象处于偏向模式时,对象头会记录获得锁的线程,这样当该线程再次或得锁时,通过 Mark Word 的线程信息就可以判断当前线程是否持有偏向锁:
JavaThread* | epoch | age | 1 | 01

缺点:偏向锁在锁竞争激烈的情况下优化效果不大,因为大量的竞争会导致持有锁的线程不停地切换,锁很难一致保持偏向模式。

偏向锁是在无竞争条件的情况下把整个同步都消除掉

轻量级锁

如果偏向锁失败,JVM 会让线程申请轻量级锁,轻量级锁在 JVM 中使用一个称为BasicObjectLock对象实现,这个对象内部由一个BasicLock对象和一个持有该锁的 Java对象指针 组成。

BasicObjectLock对象放置在Java栈的栈帧中。
BasicObjectLock锁对象的对象头部 Mark Word:
[ptr | 00 ] locked
整个 Mark Word 为指向BasicLock的指针。

轻量级锁是在无竞争条件的情况下使用CAS操作去消除同步使用的操作系统的互斥量

重量级锁

锁膨胀:当轻量级锁失败,JVM 就会使用重量级锁。重量级锁对象的对象头部 Mark Word:
[ptr | 10] monitor
整个 Mark Word 表示指向monitor对象的指针,线程很可能在操作系统层面挂起,线程间切换和调度的成本就会比较高。

自旋锁

自旋锁可以使得线程尽快进入临界区而避免被操作系统挂起。

自旋锁可以使线程在没有获得锁时,不被挂起,转而去执行一个空循环(自旋),在若干个循环后,线程如果可以获得锁,则继续执行。如果依然不能获得锁,才会被挂起。

缺点:对于锁竞争激烈,单线程锁占用时间长的并发程序,自旋锁在自旋等待后,往往依然无法获得锁,浪费了 CPU 时间。

锁消除

锁消除是 JVM 在 JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁。

应用层的锁优化

在应用层合理地进行锁的优化,对系统性能也是有积极作用的。

减少锁持有时间

在锁竞争过程中,单个线程持有锁的时间越长,锁的竞争程度也就越激烈。

例如:尽量使用synchronized同步块,而不是synchronized同步方法。

减小锁粒度

减小锁粒度,是指缩小锁定对象的范围,从而减少锁冲突的可能性。

例如:ConcurrentHashMap,在 JDK 1.8 之前使用的是 分段锁 机制,对每段进行加锁而不是对整个HashMap加锁。在 JDK 1.8 抛弃了分段锁机制,而改用对表中每个桶的链表头进行加锁。

锁分离

锁分离,是减小锁粒度的一个特例,将一个独占锁分成多个锁。

例如:java.util.concurrent.LinkedBlockingQueue的实现中,采用了两把锁分离了takeput操作。

锁粗化

如果对同一个锁不停地进行 请求、同步和释放,会消耗系统资源。

JVM 在遇到一连串连续地对同一锁不断进行请求和释放的操作时,会把所有的锁操作整合对锁的一次请求,从而减少锁的请求同步次数。

无锁

是确保程序和数据线程安全的最直观的一种方式,但是在高并发时,锁的竞争可能会成为系统瓶颈。

无锁:指的是非阻塞同步的方法,不使用锁,依然能确保数据和程序在高并发时保持多线程间的一致性。

CAS(Compare And Swap)基于比较并交换的算法,在硬件层面上,大部分的现代处理器都已经支持原子化的 CAS 指令。Java中的java.util.concurrent.atomic包封装了许多 CAS 操作。

JDK 1.8 中新增的java.util.concurrent.atomic.LongAddr类,对数字进行计数采取了优化措施,将热点数据 value 分离成多个单元 cell,每个 cell 独自维护内部的值,当前对象的实际值由所有 cell 累计合成。

synchronized锁流程

  1. 检测 Mark Word 里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
  2. 如果不是,则使用 CAS 将当前线程的ID替换 Mark Word,如果成功则表示当前线程获得偏向锁,并置偏向标志位为1。
  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁
  4. 当前线程使用 CAS 将对象头的 Mark Word替换为指向轻量级锁的指针,如果成功,则当前线程获得锁。
  5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
  6. 如果自旋成功则依然处于轻量级状态。
  7. 如果自旋失败,则升级为重量级锁