剑指Offer(锁)——synchronized底层实现原理


实现synchronized的基础:

  • Java对象头
  • Monitor

Hotspot虚拟机中对象在内存中的布局:对象头、实例数据、对齐填充

1、对象头

主要来说一下对象头,下图是对象头的结构
在这里插入图片描述

2、Mark Work

在这里插入图片描述

3、Monitor

下面来介绍一下Monitor,Monitor是每个Java对象都持有的一把锁不过这把锁是一把看不见的锁;被称为Monitor锁、管程、监视器锁。可以将其理解为一个同步工具,可被描述为一种同步机制。

synchronized正是通过Monitor来获取对象的锁的:
在这里插入图片描述
接下来进一步来说一下synchronized底层的字节码实现原理:

写一个简单的同步代码块和同步方法使用javac去编译,然后用javap -verbose去查看编译出来的.class字节码文件。 在这里插入图片描述
1、首先来看一下同步代码块的字节码:
在这里插入图片描述
可以发现Monitor代表获取到锁和锁释放的过程,中间过程是获取到对象进行计数,这时使用synchronized获取到对象的使用权之后就可以重入Monitor。。

重入:从互斥锁设计上来说当一个线程试图操作一个由其他线程持有的对象锁的临界资源时候将会进入阻塞状态,但是当一个线程再次请求自己持有的对象锁的临界资源的时候这种情况就属于重入。

简单来说就是如果其他的线程先于当前线程获取到了Monitor使用权当前线程就会被阻塞,Monitor释放之后又计数器变为零,其他线程才有机会重入进来进而去获取锁。

2、同步方法的字节码:
在这里插入图片描述
可以看出并没有Monitor获取和释放的过程,是因为存在一个ACC_SYNCHRONIZED。。。

如果一个方法被其标志了代表拥有互斥锁,一次性只能由一个线程去访问这个同步方法,如果这个方法导致抛出异常就会去释放锁,此外都是正常结束之后去释放锁。

但是为什么有人会对synchronized嗤之以鼻呢???

  1. 早期版本中synchronized属于重量级(监视器)锁,因为Monitor属于底层锁依赖于Mutex Lock因此使用的代价很高;
  2. 线程之间切换需要从用户态切换到核心态,开销很大。

后来Hotspot JVM在JDK6之后进行了很大的提升,对互斥锁和其他锁也都进行了很高的性能提升,方法包括:

  1. Adaptive Spinning 自适应锁
  2. Lock Eilminate 锁消除
  3. Lock Coarsening 锁初始化
  4. Lightweight Locking 轻量级锁
  5. Biased Locking 偏斜锁

4、自旋锁和自适应锁

自旋锁

很多情况下共享数据的锁定状态持续时间较短切换线程时不值得的,因此可以使用自旋锁,不放弃CPU使用的情况下让线程执行忙循环等待锁释放之后再去获取锁,但是缺点也是很明显的如果长时间获取不到锁就会带来很多的性能消耗。

因此为了改善这些不足,出现了自适应锁。

5、自适应锁

JDK6中出现了自适应锁这代表自旋的次数不再是固定的,是由前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定,比如前一下线程自旋之后获取到了锁那意味着现在获取到锁的可能性很大会一直自旋,反之会放弃CPU资源之后再请求。

6、锁消除

是另外一种优化的锁,优化的更加彻底在JIT编译的时候对运行的上下文进行扫描去除不可能存在竞争的锁。

下面是一个简单的demo:
在这里插入图片描述
上面都是关于锁优化的案例,现在说一个反面的案例:锁粗化。

7、锁粗化

在使用互斥锁时候要让锁的范围尽量小,因为加锁和释放锁的性能消耗是很大的,但是也存在是希望在循环中加锁的这就会导致不可避免的麻烦,想到的解决方案就是将整个循环包围起来避免重复加锁和释放锁。

8、synchronized的四种状态

现在再来说一个比较关键的问题,就是synchronized的四种状态:无锁、偏斜锁、轻量级锁和重量级锁。在竞争级别上升的时候锁的级别也会上升,反之锁会进行降级,存在闲置的Monitor就会进行降级。

锁膨胀的方向:无锁->偏斜锁->轻量级锁->重量级锁

偏斜锁

大多数情况下锁不存在多线程竞争,总是由一个线程多次获得。

核心思想:

如果一个线程获取到锁,锁进行了偏斜模式,此时Mark Word的结构也编程偏斜锁结构当该线程再次请求锁的时候不需要再做任何的同步操作,即获取锁的过程只需要检查Mark Word的锁标记为为偏向锁以及当前线程ID等于Mark Word的Thread ID即可,省去了大量有关锁的申请操作。

需要注意的是大多数情况下还是同一个线程去申请锁,实际上synchronized默认级别就是偏斜锁当锁竞争激烈的时候锁会膨胀为轻量级锁。

轻量级锁

轻量级锁来源于偏斜锁,当偏斜锁运行在一个线程进入同步快的情况下,第二个线程加入锁竞争的时候就会升级了。

适用场景:线程需要交替执行同步快的时候。

偏斜锁的加锁过程:

  1. 代码进入同步快时候如果同步对象锁状态为无锁状态(锁标志位01标志)。虚拟机首先将在当前线程栈帧中建立一个名为锁记录(Lock Record)的空间,同于存储对象目前的Mark Word的拷贝,官方称为Displaced Mark Word。线程堆栈与对象头的状态如图:
    在这里插入图片描述
  2. 之后拷贝对象中的Mark Word复制到锁记录中;
  3. 当拷贝成功之后虚拟机会使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record中的owner指针指向Object Mark Word。更新成功执行步骤4,否则执行步骤5;
  4. 如果更新成功,那么线程拥有了这个对象的锁,并且对象的Mark Word锁标志设置为00,代表对象处于轻量级锁状态,栈帧和对象头的状态如图:
    在这里插入图片描述
  5. 如果这个更新失败了,JVM就会首先检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态变为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线程便尝试使用自旋来获取锁。

解锁过程:

  1. 通过 CAS操作尝试把线程中复制的DIsplaced Mark Word对象替换当前的Mark Word;
  2. 如果替换成功,整个同步过程就完成了;
  3. 如果替换成功,说明有其他线程尝试获取该锁(此时锁已膨胀),那就要在释放锁的同时唤醒被挂起的线程。

在这里插入图片描述

发布了242 篇原创文章 · 获赞 23 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_44240370/article/details/104118244
今日推荐