偏向锁:提高无竞争情况下的性能

  • 偏向锁是Java6中引入的一项锁优化措施,目的是为了提高程序在无竞争情况下的运行性能。他的目标是消除同步操作所需的开销,进一步提升性能。更具体地说,偏向锁通过消除在无竞争情况下的同步原语(为实现线程同步所采取的措施),来减少锁的开销。

  • 偏向锁的工作原理

    1. 先普及一下对象头的相关信息。HostSpot虚拟机的对象头分为两部分,第一部分用于存储对象自身的运行时数据,如哈希码,GC分代年龄等。这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特,官方称他为”Mark Word“。这部分是实现轻量级锁和偏向锁的关键。另外还有一部分用于存储指向方法区对象类型数据的指针。由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效率,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。他还会根据对象的状态复用自己的存储空间。在对象未被锁定的状态下,Mark Word的32个比特空间里的25个比特将用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,还有1个比特固定为0(这表示未进入偏向模式)。【下图为Mark Word布局】

    2. 当一个线程第一次获取一个锁对象时,虚拟机会将该对象的头部标记为偏向模式。这意味着锁对象的标志位被设置为 01,并记录持有锁的线程的 ID。

    3. 这时,虚拟机使用CAS操作将线程的ID存储在对象的”Mark Word“中。存储的空间占用了对象HashCode存放的空间。

  • 偏向锁的优势

    1. 在持有偏向锁的线程继续执行同步块时,虚拟机可以跳过所有的同步操作(如采用CAS对 Mark Word 的更新)。这样可以显著减少同步操作的开销。

  • 偏向锁的撤销

    1. 如果另一个线程尝试获取已被偏向的锁对象,偏向锁会被撤销。撤销后的锁对象会根据当前状态转变为轻量级锁或重量级锁。

    2. 举一个例子来详细说明锁撤销的流程:假设有线程t1、线程t2和线程t3,刚开始锁对象的Mark Word中无记录(null),线程
      t1访问了锁对象,经过检查,发现发现Mark Word中的线程ID与自己不匹配,执行CAS操作修改为自己的线程ID,接下来
      t1再次访问了锁对象,经过检查,发现Mark Word中的线程ID与自己匹配,直接进入同步代码块
      t2访问了锁对象,经过检查,发现Mark Word中的线程ID与自己不匹配,偏向的是t1,于是执行CAS操作:在全局安全点时,暂停了线程t1检查状态,发现t1已经停止(执行完毕或异常终止),,Mark Word更改为t2的线程ID
      t2再次访问了锁对象,经过检查,发现Mark Word中的线程ID与自己匹配,直接进入同步代码块
      t3访问了锁对象,经过检查,发现Mark Word中的线程ID与自己不匹配,偏向的是t2,于是执行CAS操作:在全局安全点时,暂停了线程t2检查状态,发现t2仍然活跃,t3修改失败,锁升级为轻量级锁,升级后锁仍然由t2持有;t2被唤醒继续执行,t3保持等待,之后按照轻量级锁执行。全局安全点:当前所有线程都已达到这个点或处于某个安全状态,以便在这个点进行操作而不会导致数据不一致或程序状态不稳定。

  • 对象哈希码与偏向锁

    1. 前面提到当使用偏向锁时,获取到锁的线程ID会占用原本用于存放对象哈希码的空间。对于计算过哈希码的对象,该对象的哈希码应该保持不变(为了保证使用对象作为键存储基于哈希的集合中时的一致性)。如果对象在偏向状态下收到计算哈希码的请求,偏向状态会被立即撤销,锁会升级为重量级锁。

总结:偏向锁通过消除无竞争情况下的同步开销,提高了程序性能。虽然它在无竞争的场景下效果显著,但在锁频繁被多个线程访问的情况下,偏向锁的效益可能有限。因此,根据具体应用场景选择合适的锁机制至关重要。