likes
comments
collection
share

Synchronized-偏向锁

作者站长头像
站长
· 阅读数 11

偏向锁是什么?

是jdk1.6引入的一种锁优化方式。让 锁对象 偏心于第一次获取锁的线程,记住它的id,当下一次再有线程获取锁的时候,与记录的ID匹配,直接获取锁就行。是一种load-and-test的过程。

引入目的?

基础库中很多类为了线程安全,添加了synchronized进行同步,保证线程安全。很多时候,我们在使用这些类的对象时,都是单线程进行访问。轻量级锁的获取锁和释放锁涉及多次CAS操作,而偏向锁只依赖一次CAS。比轻量级锁更加轻量。

JDK1.6默认开启,JDK15后默认关闭,可以通过-XX:-UseBiasedLocking进行配置启用还是禁用。

优点

  • 加锁解锁无需额外的消耗,和非同步方法时间相差纳秒级别

缺点

  • 如果存在多个线程同时访问该锁对象,则会带来额外的锁撤销的消耗。

前置知识

此处为语雀内容卡片,点击链接查看:www.yuque.com/fanxianglic…

对象状态

以下状态默认为开启偏向锁配置

对象创建

当开启偏向锁时,类的prototype_header中的锁标志位为01,是否可偏向locked_bias1,thread id为0,表示匿名偏向(anonymously biased)。

出于性能的考虑,在JVM启动后的头4秒,偏向锁配置是关闭的,locked_bias0,以禁止实例化对象被偏向。4秒后,prototype_header.locked_bias会重新设置为1,新的对象就可以被偏向绑定了。

可通过-XX:BiasedLockingStartupDelay=0将延迟改为0。即偏向锁设置在JVM一启动就生效。

锁状态

未绑定不可偏向

此时locked_bias为0,如果有线程获取此锁,会升级为轻量级锁,如果有多个线程尝试获取此锁,会升级成重量级锁。

Synchronized-偏向锁

什么时候会变成此状态呢?
  • 计算对象的hashCode,调用Object.hashCodeSystem.identityHashCode(Object)
    • 对象的hashCode并不是一创建就计算好的,而是在调用hashCode方法后,才存储在对象头中的。
    • 一旦对象进行hashCode计算,都会从可偏向状态转为此状态。如果正在偏向,则会发生锁撤销操作。
  • 偏向锁被禁用
    • jvm参数-XX:-UseBiasedLocking将偏向锁禁用
    • jvm启动头4秒
    • 发生批量锁撤销后
      • 当该类所有对象发生锁撤销的次数达到40次后,会触发批量锁撤销。将prototype_header中的locked_bias设置为0,并对所有该类对象触发锁撤销操作。以后新创建的对象,都是不可偏向的状态。
  • 偏向锁被撤销
    • 当锁对象已偏向某个线程,此时有另外一个线程来访问该锁对象,且不可重偏向则会发生锁撤销。

    • 批量锁撤销时

可偏向

locked_bias1

匿名偏向

Synchronized-偏向锁

Mark Word中的Thread ID偏向线程为0时,为匿名偏向。

意味着该锁还没有偏向于任何线程。第一个试图获得锁的线程,使用CAS指令就可将线程绑定在此锁对象上。

这是允许偏向对象的初始状态。

已偏向

Synchronized-偏向锁

Thread ID不为空,且对象头的Mark Word.epochKlass._prototype_header.epoch相等。表示当前锁已偏向于某个线程,并且不可重偏向。如果当前线程不是锁对象偏向的线程,则触发锁撤销。升级成轻量级锁。

可重偏向

Synchronized-偏向锁

Thread ID不为空,但是对象头的Mark Word.epochKlass._prototype_header.epoch不相等。代表当前锁对象的偏向已过期,可进行重偏向。使用CAS将新线程绑定于当前锁对象。

加锁过程

Synchronized-偏向锁

  1. 验证Mark Word.locked_bias
    1. 0,不可偏向,升级为轻量级锁
  1. 验证Klass.prototype_header.locked_bias
    1. 0,则该类所有对象全部不允许偏向,需要重置该类所有对象的locked_bias位,该类所有对象Mark Word替换为无锁标识,当前对象升级为轻量级锁。
  1. 比较对象和原型epoch位,即Mark Word.epoch == Klass._prototype_header.epoch
    1. 不想等,代表该锁的偏向已过期,可进行重偏向。进行重偏向获取偏向锁。
    2. 相等,代表该锁已偏向某个线程。继续第4步重入或者升级为轻量级锁
  1. 检验owner线程 比较偏向线程id和当前线程id
    1. 匹配,进行重入锁,向当前线程栈中写入一条Displaced Mark Word为空,Owner指向锁对象的Lock Record
    2. 不匹配,使用CAS进行线程替换(0->当前线程)。如果替换失败,需要进行线程撤销,替换Mark Word为无锁状态,并升级为轻量级锁。根据偏向线程是否还在执行同步代码块,判断是哪个线程获取升级后的轻量级锁。
      1. 偏向线程正在执行同步代码块,则偏向线程获取该锁。
      2. 偏向线程未处于活动状态或退出同步代码块,则当前线程获取轻量级锁。

解锁过程

偏向锁加锁,会向获取到锁的线程栈中插入一条Lock Record,其中的属性Displaced Mark Word为null,Owner指向当前锁对象。

偏向锁的解锁,是将线程栈中的Lock Record.Owner设置为null即为解锁。

Synchronized-偏向锁

Synchronized-偏向锁

锁撤销

  1. Mark word设置为无锁状态,并升级为轻量级锁。
    1. 如果偏向线程还在执行同步代码块,则偏向线程获取该轻量级锁,当前线程进行轻量级锁升级重量级锁的自旋等待。
    2. 如果偏向线程已执行完同步代码块或未处于活动状态,则当前线程获取升级后的轻量级锁。

批量重偏向

bulk rebias

避免短时间内大量偏向锁的撤销。例如一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。当执行批量重偏向后,如果原偏向锁持有者线程不再执行同步块,则锁可以偏向于新线程。

Klass中维护了一个偏向锁撤销计数器revocation_count,每次该类的对象发生偏向锁撤销操作时,该计数器+1,当达到阈值20时,JVM会认为该偏向锁存在问题,给该偏向锁一个重偏向的机会。将Klass.prototype_header.epoch+1。并会对该类的所有对象的偏向状态进行初始化。

  • 如果该对象的偏向线程当前持有该锁对象,则更新mark word.epoch=_prototype_header.epoch,表示该锁对象已偏向。
  • 其他对象不更新,表示该对象偏向已过期,可进行重新偏向。

批量锁撤销

revocation_count达到阈值40时,JVM认为该偏向锁,存在多线程竞争关系。

但是在彻底禁用偏向锁之前,JVM还给了一次改过自新的机会,存在另外一个计时器BiasedLockingDecayTime = 25000

  1. 如果在距离上次批量重偏向发生的 25 秒之内,并且累计撤销计数达到40,就会发生批量撤销(偏向锁彻底 game over)
  2. 如果在距离上次批量重偏向发生超过 25 秒之外,那么就会重置在[20,40)内的计数, 再给次机会。

禁用偏向锁过程:更新klass._prototype_header.locked_bias为0,之后对于该class对象的锁,直接走轻量级锁逻辑。

如何查询Mark Word

  1. openjdk提供了一个库查看内存布局工具JOL(Java object layout)JOL maven 仓库地址
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>
  1. 其中ClassLayout.parseInstance(o).toPrintable()可以打印出Mark word信息。查看锁状态。

示例

package com.learn.project;

import org.openjdk.jol.info.ClassLayout;

/**
 * MarkWordTest
 *
 * @author chenfuyuan
 * @date 2022/12/10 19:01
 */
public class MarkWordTest {

    public static void main(String[] args) throws InterruptedException {
        User initBeforeBiasSetting = new User();
        System.out.println("设置可偏向之前:"+ printClassLayout(initBeforeBiasSetting));
        Thread.sleep(5000L);
        User initAfterBiasSetting = new User();
        System.out.println("设置可偏向之后:"+ printClassLayout(initAfterBiasSetting));
        synchronized (initAfterBiasSetting) {
            //进入同步代码块
            System.out.println("进入同步代码块,进行偏向绑定:" + printClassLayout(initAfterBiasSetting));
        }
        Thread thread01 = new Thread(() -> {
            synchronized (initAfterBiasSetting) {
                System.out.println("被另外一个线程调用后(thread):" + printClassLayout(initAfterBiasSetting));
            }
        });
        thread01.start();
        Thread.sleep(1000L);
        System.out.println("被另外一个线程调用后(main):" + printClassLayout(initAfterBiasSetting));
    }

    private static String printClassLayout(Object obj) {
        return ClassLayout.parseInstance(obj).toPrintable();
    }
}


===================输出=================
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/bin/java -javaagent:/Users/chenfuyuan/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.7571.182/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=58794:/Users/chenfuyuan/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/223.7571.182/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/rt.jar:/Users/chenfuyuan/code/learn/java/learn-jdk/learn-juc/target/classes:/Users/chenfuyuan/resource/maven_repository/learn/org/openjdk/jol/jol-core/0.16/jol-core-0.16.jar com.learn.project.MarkWordTest
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
设置可偏向之前:com.learn.project.User object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf800c143
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

设置可偏向之后:com.learn.project.User object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf800c143
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

进入同步代码块,进行偏向绑定:com.learn.project.User object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00007fe28f809005 (biased: 0x0000001ff8a3e024; epoch: 0; age: 0)
  8   4        (object header: class)    0xf800c143
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

被另外一个线程调用后(thread):com.learn.project.User object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00000003061f89e8 (thin lock: 0x00000003061f89e8)
  8   4        (object header: class)    0xf800c143
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

被另外一个线程调用后(main):com.learn.project.User object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0xf800c143
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

注意事项

  1. 必须调用Object.hashCode才会进行锁撤销。重载的hashCode方法无法触发锁撤销。
  2. 在同步代码块中调用hashCode,锁对象会直接升级成重量级锁。 Synchronized-偏向锁

待补充的知识

  • 轻量级锁
  • 重量级锁
  • 安全点 safe point

参考文献