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的三把锁的主要内容,如果未能解决你的问题,请参考以下文章