写点什么

oh,老哥,是码友就来看这篇多线程,mybatis 的底层原理

用户头像
极客good
关注
发布于: 刚刚

Exception table:from to target type5 15 18 any18 21 18 anyLineNumberTable:line 17: 0line 18: 5line 19: 13line 20: 23StackMapTable: number_of_entries = 2frame_type = 255 /* full_frame /offset_delta = 18locals = [ class com/design/model/singleton/SynchronizeDetail, class java/lang/Object ]stack = [ class java/lang/Throwable ]frame_type = 250 / chop */offset_delta = 4}


观察一下编译后的代码,在 testRoller()方法中有这样一行描述 flags: ACC_PUBLIC, ACC_SYNCHRONIZED,表示着当前方法的访问权限为 SYNCHRONIZED 的状态,而这个标志就是编译后由 JVM 根据 Synchronized 加锁的位置增加的锁标识,也称作类锁,凡是要执行该方法的线程,都需要先获取 Monitor 对象,直到锁被释放以后才允许其他线程持有 Monitor 对象。以 HotSport 虚拟机为例 Monitor 的底层又是基于 C++ 实现的 ObjectMonitor,我不懂 C++,通过查资(百)料(度)查到了这个 ObjectMonitor 的结构如下:


ObjectMonitor::ObjectMonitor() {


_header = NULL;


_count = 0;


_waiters


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


= 0,


_recursions = 0; //线程重入次数_object = NULL;


_owner = NULL; //标识拥有该 monitor 的线程_WaitSet = NULL; //由等待线程组成的双向循环链表_WaitSetLock = 0 ;


_Responsible = NULL ;


_succ = NULL ;


_cxq = NULL ; //多线程竞争锁进入时的单向链表 FreeNext = NULL ;


_EntryList = NULL ; //处于等待锁 block 状态的线程的队列,也是一个双向链表_SpinFreq = 0 ;


_SpinClock = 0 ;


OwnerIsThread = 0 ;


}


那么接下来就用一张图说明一下多线程并发情况下获取 testRoller()方法锁的过程



上文中提到了 MutexLock,而图中加解锁获取 Monitor 对象就是基于它实现的互斥操作,再次强调,在加解锁过程中线程会存在内核态与用户态的切换,因此牺牲了一部分性能。


再来说一下 testRunning()方法,很显然,在编译后的 class 中出现了一对 monitorenter/monitorexit,其实就是对象监视器的另一种形态,本质上是一样的,不过区别是,对象在锁实例方法或者实例对象时称作内置锁。而上面的 testRoller()是对类(对象的 class)的权限控制,两者互不影响。


到这里就解释 Synchronized 的基本概念,接下来要说一说它到底跟对象在对空间的内存布局有什么关系。

Synchronized 与对象堆空间布局

还是以 64 位操作系统下 HotSport 版本的 JVM 为例,看一张全网都搜的到的图



图中展示了 MarkWord 占用的 64 位在不同锁状态下记录的信息,主要有对象的 HashCode、偏向锁线程 ID、GC 年龄以及指向锁的指针等,记住这里的 GC 标志记录的位置,将来的 JVM 文章也会用到它,逃不掉的。在上篇例子中查看内存布局的基础上稍微改动一下,代码如下:


/**


  • FileName: JavaObjectMode

  • Author: RollerRunning

  • Date: 2020/12/01 20:12 PM

  • Description:查看加锁对象在内存中的布局*/public class JavaObjectMode {public static void main(String[] args) {//创建对象 Student student = new Student();synchronized(student){// 获得加锁后的对象布局内容 String s = ClassLayout.parseInstance(student).toPrintable();// 打印对象布局 System.out.println(s);}}}


class Student{private String name;private String address;


public String getName() {return name;}


public void setName(String name) {this.name = name;}


public String getAddress() {return address;}


public void setAddress(String address) {this.address = address;}}


第一张图是上篇文章的也就是没加锁时对象的内存布局,第二张图是加锁后的内存布局,观察一下 VALUE 的值




其实加锁后,就是修改了对象头中 MarkWord 的值用来记录当前锁状态,所以可以看到加锁前后 VALUE 发生了变化。 从第一张图的第一行 VALUE 值可以看出当前的锁标记为 001(这里面涉及到一个大端序和小端序的问题,可以自己学习一下:https://blog.csdn.net/limingliang_/article/details/80815393 ),对应的表中恰好是无锁状态,实际代码也是无锁状态。而图二可以看出当前锁标记为 000(提示:在上图 001 同样的位置),对应表中状态为轻量级锁,那么代码中的的 Synchronized 怎么成了轻量级锁了呢?因为在 JDK1.6 以后对锁进行了优化,Synchronized 会在竞争逐渐激烈的过程中慢慢升级为重量级互斥锁。


但是还有问题,为啥加锁了,上来就是轻量级锁而不是偏向锁呢,原因是在初始化锁标记时 JVM 中默认延迟 4s 创建偏向锁,由-XX:BiaseedLockingStartupDelay=xxx 控制。一旦创建偏向锁,在没有线程使用当前偏向锁时,叫做匿名偏向锁,即上表中偏向线程 ID 的值为空,当有一个线程过来加锁时,就进化成了偏向锁。


到这里,是不是已经能看明白天天说的锁也不过是一堆标志位实现的,让我写几个 if-else 就给你写出来了


Synchronized 锁升级过程

锁的升级过程为:偏向锁-->偏向锁-->轻量级锁-->重量级锁。这个过程是随着线程竞争的激烈程度而逐渐变化的。

偏向锁

其中匿名偏向锁前面已经说过了,偏向锁的作用就是当同一线程多次访问同步代码时,这一线程只需要获取 MarkWord 中是否为偏向锁,再判断偏向的线程 ID 是不是自己,就是俩 if-else 搞定,Doug Lee 先生不过如此嘛。如果发现偏向的线程 ID 是自己的线程 ID 就去执行代码,不是就要通过 CAS 来尝试获取锁,一旦 CAS 获取失败,就要执行偏向锁撤销的操作。而这个过程在高并发的场景会造成代码很大的性能开销,慎重使用偏向锁。图为偏向锁的内存布局


轻量级锁

轻量级锁是一种基于 CAS 操作的,适用于竞争不是很激烈的场景。轻量级锁又分为自旋锁和自适应自旋锁。自旋锁:因为轻量锁是基于 CAS 理论实现的,因此当资源被占用,其他线程抢锁失败时,会被挂起进入阻塞状态,当资源就绪时,再次被唤醒,这样频繁的阻塞唤醒申请资源,十分低效,因此产生了自旋锁。JDK1.6 中,JVM 可以设置-XX:+UseSpinning 参数来开启自旋锁,使用-XX:PreBlockSpin 来设置自旋锁次数。不过到了 JDK1.7 及以后,取消自旋锁参数,JVM 不再支持由用户配置自旋锁,因此出现了自适应自旋锁。自适应自旋锁:JVM 会根据前一线程持有自旋锁的时间以及锁的拥有者的状态进行动态决策获取锁失败线程的自旋次数,进而优化因为过多线程自旋导致的大量 CAS 状态的线程占用资源。下图为轻量级锁内存布局:



随着线程的增多,竞争更加激烈以后,CAS 等待已经不能满足需求,因此轻量级锁又要向重量级锁迈进了。在 JDK1.6 之前升级的关键条件是超过了自旋等待的次数。在 JDK1.7 后,由于参数不可控,JVM 会自行决定升级的时机,其中有几个比较重要的因素:单个线程持有锁的时间、线程在用户态与内核态之间切换的时间、挂起阻塞时间、唤醒时间、重新申请资源时间等

重量级锁

而当升级为重量级锁的时候,就没啥好说的了,锁标记位为 10,所有线程都要排队顺序执行 10 标记的代码,前面提到的每一种锁以及锁升级的过程,其实都伴随着 MarkWord 中锁标记位的变化。相信看到这,大家应该都理解了不同时期的锁对应着对象在堆空间中头部不同的标志信息。重量级锁的内存布局我模拟了半天也没出效果,有兴趣的大佬可以讲一下。


最后附上一张图,展示一下锁升级的过程,画图不易,还请观众老爷们关注啊:


锁优化

1.动态编译实现锁消除


通过在编译阶段,使用编译器对已加锁代码进行逃逸性分析,判断当前同步代码是否是只能被一个线程访问,未被发布到其他线程(其他线程无权访问)。当确认后,会在编译器,放弃生成 Synchronized 关键字对应的字节码。


2.锁粗化

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
oh,老哥,是码友就来看这篇多线程,mybatis的底层原理