当前位置: 首页 > news >正文

synchronized底层原理

目录

  • 概述
  • 一、理解
    • 1. synchronized对MESA管程模型的实现
    • 2. 为什么用cxq和EntryList两个队列存放线程
  • 二、对象结构
    • 1. Mark Word
      • 轻量级锁的MarkWord指向栈中lockRecord的指针
      • 重量级锁的MarkWord指向堆中Monitor的指针
    • 2. Klass Pointer
    • 3. 数组长度(可选)
  • 三、锁优化策略
    • 1. 偏向锁
    • 2. 轻量级锁
    • 3. 重量级锁
    • 4. 锁升级过程
    • 5. 几种锁状态的总结
    • 6. 其他的锁优化
      • 自旋锁和适应性自旋锁
      • 锁消除
      • 锁粗化
  • 四、与AQS体系锁的对比
    • 1. LockSupport.park和synchronized的重量级锁一样吗
    • 2. 为什么ReentrantLock比synchronized性能高
    • 3. 和ReentrantLock的对比
  • 参考

概述

synchronized能够实现线程同步。无论怎么使用,最终都是对对象加锁。

  • 锁class对象、静态方法 都是锁类对象
  • 锁普通对象、普通方法,都是锁实例对象

为什么synchronized最终都是作用在对象上呢? 因为对象在堆中除了除了有字段属性外,还有固定的对象头,通过对象头最终可以得知这个对象是否被加过锁,以及持有锁的线程是谁。(第2节 对象结构)

仔细研究对象头后,发现其中记录了多种锁的状态。锁的升级与其息息相关。(第3节 锁优化策略)

许多线程长时间阻塞,说明锁已经升级到了重量级锁,JVM使用Monitor处理阻塞线程,如wait、notify等。(第1节 Monitor阻塞机制)

AQS也是参照jvm底层Monitor处理方式实现的,但是性能上更优一些。(第4节 与AQS的对比)

一、理解

synchronized是非公平、可重入、独占锁

wait和notify使用时线程必须持有锁

1. synchronized对MESA管程模型的实现

在这里插入图片描述
底层逻辑:

  • cxq、EntryList都是先进后出队列FILO
  • 争抢锁失败的线程会进入cxq
  • 获取锁的线程调用wait后进入waitSet
  • waitSet中被notify唤醒的线程会进入cxq
  • 持有锁的线程释放锁后
    • 唤醒EntryList最后入队的线程
    • 如果EntryList没有节点,则会将cxq的节点移动过来,再唤醒最后入队的线程

下面是代码证明。
在这里插入图片描述

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * <p> Description:Application</p>
 * <p> CreationTime: 2022/1/20 10:39
 *
 * @author dreambyday
 * @since 1.0
 */
public class Application {
    public synchronized static void show(int seconds) {
        try {
            TimeUnit.MILLISECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }

    public static void testSync() throws InterruptedException {
        // 0->200ms 1获取锁 234blocked  -> 234在cxq
        new Thread(()->Application.show(200),"1").start();
        // 保证1线程先获取锁
        TimeUnit.MILLISECONDS.sleep(10);
        // 200->300ms 1释放锁 234从cxq进入EntryList,先进后出,4获取锁,23继续blocked
        new Thread(()->Application.show(400),"2").start();
        new Thread(()->Application.show(400),"3").start();
        new Thread(()->Application.show(400),"4").start();
        // 300->(200+400)ms
        // 4继续获取锁,23blocked,处于EntryList。
        // 567线程启动,获取锁失败,进入cxq
        TimeUnit.MILLISECONDS.sleep(300);
        new Thread(()->Application.show(400),"5").start();
        new Thread(()->Application.show(400),"6").start();
        new Thread(()->Application.show(400),"7").start();
        // EntryList先被唤醒,EntryList空了后,将cxq整体移动到EntryList继续唤醒
        // 200->(200+400*3)ms 处于EntryList的线程依次被唤醒,顺序为432
        // (200+400*3)ms 时,EntryList为空,cxq移动到EntryList,内容为567
        // (200+400*3)ms到结束,处于EntryList的线程依次被唤醒,顺序为765
    }
    public static void main(String[] args) throws InterruptedException {
    	// 最终顺序为1432765
        testSync();
    }
}


2. 为什么用cxq和EntryList两个队列存放线程

为了降低cxq尾部并发竞争

假如只有cxq先进后出队列,队列尾部面临的操作有

  • 增加新的竞争锁失败的线程
  • 尾部线程被唤醒
  • 增加从waitSet中被notify的线程

拆分两个队列后,唤醒操作发生在EntryList上

二、对象结构

在这里插入图片描述

new Object()在内存中的字节数为16 = 8(Mark Word)+4(开启压缩指针的klass pointer) + 0(不是数组,不占字节) + 0(对象体,无属性,不占字节) + 4(对齐填充,保证整体是8的倍数)

计算各种对象大小看这篇文章,包括基础类型数组、String等

1. Mark Word

占1个字宽大小(32位机则为32位长,64位机为64位长)。是实现轻量级锁和偏向锁的关键。

包括: hashcode、分代年龄、是否偏向、锁的标志、GC标记、指向monitor的指针、指向持有锁线程的lockRecord的指针、偏向锁线程ID、epoch(第几代偏向锁)

Mark Word存在5种状态。如下图。
在这里插入图片描述
图片来源

轻量级锁的MarkWord指向栈中lockRecord的指针

虚拟机栈保存lockRecord: 记录了持有锁的线程、无锁状态下的Mark Word信息(为了备份还原,以及记录hashcode等信息)

锁重入情况: 栈新压入Mark Word记录为null的lockRecord,释放一次锁就从栈弹出一次lockRecord

重量级锁的MarkWord指向堆中Monitor的指针

在这里插入图片描述
monitor对象和java对象同生共死,存储在堆中。重量级锁状态下,monitor同样保存了无锁状态的MarkWord信息。

2. Klass Pointer

  • 32位机则为32位长,64位机默认指针压缩开启,也占32位。不开启则64位。
  • 指向元空间(jdk8)或方法区(jdk7)中的类的元数据,用于标识当前实例对象属于哪个类

为什么指向元数据而不是类对象,我只能靠一个例子理解:如果锁的对象是类对象,那么klass pointer相当于自己指向自己,会令人奇怪吧

3. 数组长度(可选)

32位机和64位机都是32位长,因此数组长度理论上不能超过 2 32 2^{32} 232 。实际上,根据JVM对对象头的处理,目前数组的最大长度是 2 31 − 1 2^{31}-1 2311 ,即Integer最大值

三、锁优化策略

锁级别: 无锁->偏向锁->轻量级锁->重量级锁

锁只能升级不能降级。

1. 偏向锁

偏向锁是加锁操作的优化手段。为了消除数据无竞争下的锁重入开销引入偏向锁。在无锁竞争场合,偏向锁性能较好。

偏向锁使用了一种等到竞争出现才释放锁的机制,消除偏向锁的开销比较大

自JDK6,JVM默认启动偏向锁模式,但是会在虚拟机启动后延迟4s左右才会开启

在启动偏向锁后创建的对象,对象头的Mark Word的锁状态默认变成偏向锁状态,并且Thread ID为0 。此时处于 可偏向但未偏向任何线程 ,也叫匿名偏向状态

为什么要有延迟偏向: 虚拟机启动过程中,许多后台线程可能会争抢锁,导致对象头的锁状态从偏向锁撤销,再升级到 轻量级锁或重量级锁。

  • 无锁时在MarkWord存储hashcode。
  • 偏向锁无位置存储hashcode。
  • 轻量级锁在栈的锁记录中记录hashcode
  • 重量级锁在Monitor中记录hashcode

对象可偏向或已偏向时,调用hashcode会使对象无法偏向。

  • 可偏向时,调hashcode,偏向锁撤销,并只能升级为轻量级锁
  • 已偏向时,调hashcode,偏向锁撤销并升级为重量级锁

几种情况:

  • 创建对象默认无锁->synchronized加锁->无锁变轻量级锁
  • 创建对象默认偏向锁->调用hashcode->撤销偏向锁变为无锁->synchronized加锁->无锁变轻量级锁
  • 创建对象默认偏向锁->synchronized加锁->调用hashcode->偏向锁撤销变重量级锁

偏向锁状态,执行

  • notify会升级为轻量级锁
  • wait会升级为重量级锁

2. 轻量级锁

轻量级锁适用于锁竞争不激烈的场景。 如两个线程交替运行

多线程竞争同一把锁(偏向锁或轻量级锁)时,竞争失败的线程若在短暂的自旋后获取到锁,则不会导致锁膨胀为重量级锁。

3. 重量级锁

重量级锁适用于锁竞争激烈或同步块执行时间长的场景

重量级锁基于Monitor实现,用户态转内核态耗时较长。

4. 锁升级过程

无锁->
上图为锁竞争对于锁状态变化的影响。
参考

5. 几种锁状态的总结

优点缺点适用场景
偏向锁加解锁无额外消耗,单线程下加锁基本无消耗线程竞争产生额外的锁撤销成本单线程访问同步代码块
轻量级锁竞争线程不阻塞,基于cas自旋在用户态实现长时间锁自旋获取不到锁还是会锁膨胀,并且消耗CPU同步块执行速度快、锁竞争不激烈
重量级锁锁不自旋,不消耗cpu线程阻塞,用户态转内核态效率低追求吞吐量、竞争激烈、同步代码块执行时间长

6. 其他的锁优化

自旋锁和适应性自旋锁

  • 用于避免轻量级锁直接升级为重量级锁,用户态转内核态带来的损耗。 轻量级锁发生竞争时会先进行一段时间自旋, 超过一定自旋次数后才会升级
  • 适应性自旋锁是为了动态调节锁升级自旋次数的阈值。 虚拟机认为上次自旋成功了,这次也可能成功,允许的自旋次数会增加。反之亦然。

锁消除

锁消除的依据是逃逸分析的数据支持。如果JVM检测同步内容不会逃逸,则会直接消除加锁操作

锁粗化

连续的加锁、解锁操作合并为一个加锁解锁操作可能提升性能,因此有了锁粗化的概念。

JVM检测到对同一个对象连续的加解锁,并且可以合并时,会将加解锁操作移动到循环之外。

for x : xx
	synchronized(obj) {}

四、与AQS体系锁的对比

1. LockSupport.park和synchronized的重量级锁一样吗

结论: 是。LockSupport也是基于mutex实现的,有用户态和内核态的切换

ReentrantLock最终会使用LockSupport.park阻塞线程,因此最终和synchronized的重量级锁一样

2. 为什么ReentrantLock比synchronized性能高

参考

ReentrantLock基于AQS实现。AQS在Java代码层面管理阻塞队列,synchronized由jvm层面管理阻塞队列。AQS使用CAS较多,阻塞队列操作逻辑比jvm实现的好,因此性能高一些。

性能比较1
性能比较2

3. 和ReentrantLock的对比

区别synchronizedReentrantLock
修饰位置不同静态方法、普通方法、代码块代码块
自动与非自动释放锁自动释放锁手动释放锁
锁类型不同非公平锁默认非公平锁,也可以创建公平锁
响应中断不同不可以响应中断能够响应中断
底层实现不同基于JVM的Monitor基于AQS
阻塞后线程状态不同阻塞进入BLOCKED状态阻塞进入WAITING状态
同步队列实现方式不同类似栈,有两个,先进后出队列且只有一个,先进先出

参考

  • 偏向锁、轻量锁升级对对象头、哈希码的影响
  • 调用hash计算时锁的变化
  • 再谈阻塞(3):cxq、EntryList与WaitSet
  • synchronized读书笔记
  • Java精通并发-通过openjdk源码分析ObjectMonitor底层实现
  • 深入分析Synchronized原理(阿里面试题)
  • 计算各种对象大小看这篇文章,包括基础类型数组、String等

相关文章:

  • Java多线程之CAS中的ABA问题与JUC的常见类
  • 【Android -- 开源库】权限适配 RxPermissions 的基本使用
  • Unity-TCP-网络聊天功能(二): 登录与注册
  • 51单片机最强模块化封装(1)
  • Python学习-----起步2(变量与转义符)
  • Java程序设计实验3 | 面向对象(上)
  • 优秀的代码最终选择if else,还是switch case
  • Openharmony的编译构建--进阶篇1
  • 每天一道大厂SQL题【Day02】电商场景TopK统计
  • EMT4J详细介绍与使用,帮你找到Java版本升级带来的问题,让你在项目jdk升级不在头疼
  • 第2章:使用CSS定义样式
  • 【数据结构】动图详解单向链表
  • MySQL基础篇笔记
  • Vue3现状—必然趋势?
  • uniapp获取支付宝user_id - 支付宝提现 - 登录授权 - APP支付宝登陆 - H5支付宝授权
  • Promise详解与手写实现
  • 【C++】类型转换
  • 关于栈和队列
  • 网络知识详解之:网络攻击与安全防护
  • Java快速上手Properties集合类
  • https://app.hackthebox.com/machines/Inject
  • Spring —— Spring简单的读取和存储对象 Ⅱ
  • 渗透测试之冰蝎实战
  • Mybatis、TKMybatis对比
  • Microsoft Office 2019(2022年10月批量许可版)图文教程
  • 《谷粒商城基础篇》分布式基础环境搭建
  • 哈希表题目:砖墙
  • Vue 3.0 选项 生命周期钩子
  • 【车载嵌入式开发】AutoSar架构入门介绍篇
  • 【计算机视觉 | 目标检测】DETR风格的目标检测框架解读