专业的JAVA编程教程与资源

网站首页 > java教程 正文

Java并发编程(23)锁消除,锁粗化,偏向锁,轻量级锁,自旋锁

temp10 2025-05-15 20:58:37 java教程 4 ℃ 0 评论

上一篇:Java并发编程(22)引入写缓冲器和无效队列导致的可见性问题处理

synchronized锁分析前置铺垫

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

Java并发编程(23)锁消除,锁粗化,偏向锁,轻量级锁,自旋锁

实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐这个不用管;

最重点的Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:

其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构;

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

synchronized对象锁:锁标识位为10,其中指针指向的是monitor对象(ObjectMonitor)(也称为管程或监视器锁)的起始地址。每个ObjectMonitor对象都存在着一个 monitor 与之关联的,ObjectMonitor对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

synchronized锁的几种实现方式

锁消除:JIT编译器对synchronized锁做的优化,在编译的时候,JIT会通过逃逸分析技术,来分析synchronized锁对象,是不是只可能被一个线程来加锁,没有其他的线程来竞争加锁,这个时候编译就不用加入monitorenter和monitorexit的指令,相当于不加锁;

锁粗化:JIT编译器如果发现有代码里连续多次加锁释放锁的代码,会给合并为一个锁,就是锁粗化,把一个锁给搞粗了,避免频繁多次加锁释放锁;

synchronized(this) {

}
synchronized(this) {

}
synchronized(this) {

}

偏向锁:monitorenter和monitorexit是要使用CAS操作加锁和释放锁的,开销较大,因此如果发现大概率只有一个线程会主要竞争一个锁,那么会给这个锁维护一个偏好(Bias),后面他加锁和释放锁,基于Bias来执行,不需要通过CAS进行加锁释放锁,你可以认为对设置偏好的线程没有加锁;

如果有其他的线程来竞争这个锁,此时就会收回之前那个线程分配的那个Bias偏好,这时加会升级为轻量级锁;

轻量级锁:偏向锁没能成功实现,就是因为不同线程竞争锁太频繁了,此时就会尝试采用轻量级锁的方式来加锁,就是将对象头的Mark Word里有一个轻量级锁指针,尝试指向持有锁的线程,然后判断一下是不是自己加的锁,如果是自己加的锁,那就执行代码。如果不是自己加的锁,那就是加锁失败,说明有其他人加了锁,这个时候就是升级为重量级锁;重量级锁就是之前讲解的一套加锁逻辑;

适应性锁又称自旋锁:JIT编译器对锁做的另外一个优化,如果各个线程持有锁的时间很短,那么一个线程竞争锁不到,就会暂停,发生上下文切换,让其他线程来执行。但是其他线程很快释放锁了,然后暂停的线程再次被唤醒,线程会频繁地上下文切换,导致开销过大;

对这种线程持有锁时间很短的情况,是可以采取忙等策略的,也就是一个线程没竞争到锁,进入一个while循环不停等待,不会暂停不会发生线程上下文切换,等到机会获取锁就继续执行;

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表