Java的Synchronized锁-偏向锁

1,881 阅读6分钟

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

偏向锁原理

在实际场景中,如果一个同步块(或方法)没有多个线程竞争,而且总是由同一个线程多次 重入获取锁,如果每次还有阻塞线程,唤醒 CPU 从用户态转核心态,那么对于 CPU 是一种资源 的浪费,为了解决这类问题,就引入了偏向锁的概念。

偏向锁的核心原理是: 如果不存在线程竞争的一个线程获得了锁,那么锁就进入偏向状态, 此时 Mark Word 的结构变为偏向锁结构,锁对象的锁标志位(lock)被改为 01,偏向标志位 (biased_lock)被改为 1,然后线程的 ID 记录在锁对象的 Mark Word 中(使用 CAS 操作完成)。以后该线程获取锁的时,判断一下线程 ID 和标志位,就可以直接进入同步块,连 CAS 操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

消除无竞争情况下的同步原语,进一步提升程序性能,所在于没有锁竞争的场合,偏向锁有很好的优化效果。但是,一旦有第二条线程需要竞争锁,那么偏向模式立即结束,进入轻量级锁的状态。

偏向锁演示

有关使用JOL打印对象布局信息可以参考使用JOL查看对象布局

  1. 演示类 该类中有个整形变量 i ,和一个同步方法,每调用一次incr,对变量i进行+1操作。并打印对象的布局信息
public class Foo {

    private int i;

    public synchronized void incr(){
        i++;
        System.out.println(Thread.currentThread().getName()+"-"+ ClassLayout.parseInstance(this).toPrintable());
    }
}
  1. Runnable实现类 里面的run方法每次都会调用incr方法。
public class LockTest implements Runnable {

    private final Foo foo;

    LockTest(Foo foo) {
        this.foo = foo;
    }

    @Override
    public void run() {
        this.foo.incr();
    }
}
  1. 偏向锁演示

    3.1. 为什么要先睡5秒?

    JVM 在启动的时候会延迟启用偏向锁机制。JVM 默认就把偏向 锁延迟了 4000ms,这就解释了为什么演示案例首先 5 秒才能看到对象锁的偏向状态。

    如果不想等待(在代码里边让线程睡眠),可以直接通过修改 JVM 的启动选项来禁止偏向 锁延迟,其具体的启动选项如下:

    -XX:+UseBiasedLocking

    -XX:BiasedLockingStartupDelay=0

    3.2 睡完5秒后,创建了一个Foo实例,然后先打印一下对象头信息,执行incr方法,执行完后,再次打印对象头信息。

    @Test
    public void test() throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        Foo foo = new Foo();
        System.err.println(ClassLayout.parseInstance(foo).toPrintable());
        foo.incr();
        System.err.println(ClassLayout.parseInstance(foo).toPrintable());

    }

输出结果:

org.ywb.Foo object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000005 (biasable; age: 0)
  8   4        (object header: class)    0xf8011d21
 12   4    int Foo.i                     0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

main-org.ywb.Foo object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00007ff84180f005 (biased: 0x0000001ffe10603c; epoch: 0; age: 0)
  8   4        (object header: class)    0xf8011d21
 12   4    int Foo.i                     1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

org.ywb.Foo object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00007ff84180f005 (biased: 0x0000001ffe10603c; epoch: 0; age: 0)
  8   4        (object header: class)    0xf8011d21
 12   4    int Foo.i                     1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

解释:

  1. 第一次打印,从对象结构可以看出biased_lock(偏向锁)状态已经启用,值为 1 ;其 lock (锁状态)的值为 01。lock 和 biased_lock 组合在一起为 101,转换成16进制正好为5表明当前的 Foo 实例处于偏向锁状态。但是由于还没有任何线程使用,所以偏向线程为空。

  2. 第二次打印,从对象结构可以看出此时偏向锁已经已经被正在执行同步代码线程获取。对象头的MarkWord记录了该偏向线程id和偏向时间戳。

  3. 第三次打印的时候已经是线程执行完同步代码,但是可以看到对象的MarkWord仍然指向了刚刚偏向的线程,以便下次之前的线程获取到同步锁的时候,只需要判断是否和自己的线程id是否一致就可以了。也可以看出,偏向线程是没有解锁这个操作的。

偏向锁的撤销和膨胀

假如有多个线程来竞争偏向锁话,如果此对象锁已经偏向了,其他的线程发现偏向锁并不是偏向自己,则说明存在了竞争,就尝试撤销偏向锁,然后膨胀到轻量级锁。

偏向锁的膨胀

具体过程如下:

  1. 在一个安全点停止拥有锁的线程

有关安全点的文章可以参考:blog.csdn.net/weixin_3634…

  1. 遍历线程的栈帧,检查是否存在存在锁记录。如果存在锁记录的话,需要清空锁记录,使其变成无锁状态,并修复锁记录指向的Mark Word,清除其线程ID

  2. 将当前锁升级成轻量级锁。

  3. 唤醒当前线程。

所以,如果某些临界区存在两个及以上的线程竞争的话,那么偏向锁就会反而会降低性能。 在这种情况情况下,可以在启动 JVM时就把偏向锁的默认功能给关闭。

撤销偏向锁的条件

  1. 多个线程竞争偏向锁。

  2. 调用偏向锁对象的 hashcode()方法或者 System.identityHashCode() 方法计算对象的 HashCode 之后,哈希码将放置到 Mark Word中,内置锁变成无锁状态,偏向锁将被撤销。

偏向锁的膨胀

如果偏向锁被占据,一旦有第二个线程争抢这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到内置锁偏向状态,这时表明在这个对象锁上已经存在竞争了。

  1. JVM 检查原来持有该对象锁的占有线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后进行重新偏向,偏向为抢锁线程。

  2. 如果JVM 检查到原来的线程依然存活,则进一步检查占有线程的调用堆栈,是否通过锁记录持有偏向锁。如果存在锁记录,则表明原来的线程还在使用偏执锁,发生锁竞争,撤销原来的偏向锁,将偏向锁膨胀为轻量级锁。