对象头和锁
对象头在 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
的实现中,采用了两把锁分离了take
和put
操作。
锁粗化
如果对同一个锁不停地进行 请求、同步和释放,会消耗系统资源。
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锁流程
- 检测 Mark Word 里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁。
- 如果不是,则使用 CAS 将当前线程的ID替换 Mark Word,如果成功则表示当前线程获得偏向锁,并置偏向标志位为1。
- 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
- 当前线程使用 CAS 将对象头的 Mark Word替换为指向轻量级锁的指针,如果成功,则当前线程获得锁。
- 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 如果自旋成功则依然处于轻量级状态。
- 如果自旋失败,则升级为重量级锁。