9.synchronized的三把锁

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了9.synchronized的三把锁相关的知识,希望对你有一定的参考价值。

我们知道,同步锁无非是多个线程抢占一个资源,如果抢占成功就获得了锁,失败的线程则阻塞等待,直到获取到锁被释放再重新抢。貌似该过程就只有一种情况 ,但是观察上面的对象头结构,会发现里面标记了偏向锁、轻量级锁和重量级锁三种,又表示什么呢?

其实在jdk6之前,synchronized确实只有一种方式——重量级锁,其基本原理是使用底层操作系统的MutexLock来实现,该过程会把当前线程挂起,并从用户态切换到内核态。其问题是开销太大、性能不足,因为很多场景可能不需要这么重的操作。这在生活中也很常见,例如,如果平时买火车票,你只要提前一两天订好就行了,如果买春节的票就需要召唤小伙伴和七大姑八大姨,让他们一起帮你抢。前者竞争没那么大,我们使用轻量级操作就行了,而后者竞争激烈,需要使用大招。

从JDK6开始,synchronized做了很多优化工作,其中就包括上说的三种锁,其核心设计理论就是如何让线程在不阻塞的情况下保证线程安全。本节我们先看一下三种锁的特征,后面再分析其原理。

1 偏向锁

偏向锁就是在没有竞争时加的锁!这句话是不是很奇怪,没有竞争为什么要加锁?这是因为这种场景可能存在竞争的情况,我们加锁是为防范,就像上面说的买票的例子,一开始我们怕抢不到,就召唤一群人来帮忙,但是实际可能当天就没人买票,这样确实能防范可能存在的竞争问题,但是代价太大,因此我们使用代价较低的偏向锁来解决。这种按照最坏情况来处理的锁也称为“悲观锁”。

而偏向锁是“乐观思维”,思想是,线程再没有竞争的情况下访问资源时,会先通过CAS方式来抢占资源,如果成功则修改对象头的标记,也就是昭告天下“这个对象是我的”,具体操作是将偏向锁标记修改为1,锁标记修改为01,并将线程ID写入到对象中。这样其他线程再访问,发现这个对象已经被其他的抢占了,就只能先阻塞一段时间再去抢占。

那为啥叫“偏向锁”呢?假如线程A抢到了资源,如果线程B再抢则会被阻塞,但如果线程A再次抢占呢?例如代码里出现对某个资源嵌套加锁,此时该让A阻塞吗?不是的,因为资源就被A的还没释放,A再次抢占就应该直接放行。这是不是对其他线程不公平呢?是的,因此这种机制叫做偏向锁,偏向早就获得资源的锁。

我们看一个偏向锁的例子:

public class BiasedLockExample 
    public static void main(String[] args) throws InterruptedException 
        BiasedLockExample example=new BiasedLockExample();
        System.out.println("加锁之前");
        System.out.println(ClassLayout.parseInstance(example).toPrintable());

        synchronized (example)
            System.out.println("加锁之后");
            System.out.println(ClassLayout.parseInstance(example).toPrintable());
        
    

在上述代码中,BiasedLockExample演示了针对example这个锁对象,在加锁之前和之后分别打印的内存对象布局,我们看一下:

part_b_inside.chapter2_synchronzied.BiasedLockExample object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

在加锁之前,我们发现对象头中的第一个字节00000001最后三位001,其中低位的两位数表示锁标记,它的值是[01],表示当前为无锁状态。

加锁之后的我们再看一下:

part_b_inside.chapter2_synchronzied.BiasedLockExample object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           90 79 bc 0c (10010000 01111001 10111100 00001100) (213678480)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

在加锁之后,我们发现对象头中的第一个字节10010000最后000,其中低位的两位数表示锁标记,它的值是[00],表示轻量级锁。

这里不存在竞争,应该是偏向锁,那为什么是轻量级锁呢。网上说是因为偏向锁开启会先延迟4秒,需要添加参数

-XX:BiasedLockingStartupDelay=0

然后再看:

此时看到第一个字节为00000101,末尾三位是101,第一个1表示偏向锁,后面的01表示当前是偏向锁状态。

我们可以将偏向锁的执行过程归纳如下:

补充

如果你调试一下上面的例子,就会发现加锁之前打印的结果第一个字节也是00000101,但是此时并没有加锁。一种解释是此时标记表示是可偏向的状态。

2 轻量级锁

偏向锁是没有竞争时获得锁资源,这种方式比单纯的加锁性能要高,但是如果此时真的有多个线程来竞争了该让竞争失败的先阻塞,直到被唤醒再重新抢锁。那是否有效率更高的方法呢? 我们可以让没有抢到资源的线程进行一定次数的重试(自旋转),例如重复抢3次或者5次等等。如果抢到了就不用重试了, 否则继续阻塞。这就是轻量级锁。 上面进行一定次数的重试的过程就叫自旋锁,也是基于CAS方式实现的。

 当然自旋的次数不是没有限制的,一般是10次,而且JVM还会执行自适应的策略来优化。

轻量级锁也可以使用上面的BiasedLockExample来展示,将参数BiasedLockingStartupDelay取消掉展示的就是轻量级锁。

3 重量级锁

轻量级锁只适合在较短的时间里能获得锁的场景,如果长时间获取不到就不能一直自旋了,因为此时线程还占用了资源,但是什么都做不了,因此自旋到一定次数之后就要让其阻塞。

从上面的例子可以看到,如果在偏向锁、轻量级锁这些类型中无法让线程获得锁资源,那么这些没获得锁的线程最终的结果仍然是阻塞等待,直到获得锁的线程释放锁之后才能被唤醒,而在整个优化过程中,我们通过乐观锁的机制来保证线程的安全性。

下面这个例子演示了在加锁之前、单个线程抢占锁、多个线程抢占锁的场景,对象头中的锁的状态变化。

public class HeavyLockExample 
    public static void main(String[] args) throws InterruptedException 
        HeavyLockExample heavy=new HeavyLockExample();
        System.out.println("加锁之前");
        System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
        Thread t1=new Thread(()->
            synchronized (heavy)
                try 
                    TimeUnit.SECONDS.sleep(2);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
            
        );
        t1.start();
        //确保t1线程已经运行
        TimeUnit.MILLISECONDS.sleep(500);
        System.out.println("t1线程抢占了锁");
        System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
        synchronized (heavy)
            System.out.println("main线程来抢占锁");
            System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
        
        System.gc();
        System.out.println(ClassLayout.parseInstance(heavy).toPrintable());
    

 结果比较长,我们不再贴出来,读者可以从中看到锁状态的逐步升级。

以上是关于9.synchronized的三把锁的主要内容,如果未能解决你的问题,请参考以下文章

转载--理解数字信号处理的三把钥匙

从“云化”到“云原生化”,云原生 2.0的三把尖刀

HTML基础之JS

HTML基础之JS

html基础之js操作

学习记录013-HTML基础js操作