JUC之Java内置锁的核心原理

news/2024/5/20 9:55:16 标签: java, 开发语言, JUC

文章目录

      • JUC之Java内置锁的核心原理
        • Java对象结构
          • 对象头
          • 对象体
          • 对齐字节
        • Mark Word的结构信息
          • 64位Mark Word的构成
        • 偏向锁
          • 偏向锁的设置
          • 偏向锁的重偏向
          • 偏向锁的撤销
          • 偏向锁的膨胀
        • 轻量级锁
          • 执行过程
          • 轻量级锁的分类
            • 普通自旋锁
            • 自适应自旋锁
        • 重量级锁
        • 偏向锁、轻量级锁与重量级锁的对比

JUCJava_1">JUC之Java内置锁的核心原理

​ Java内置锁是一个互斥锁,这就意味着最多只有一个线程能够获 得该锁,当线程B尝试去获得线程A持有的内置锁时,线程B必须等待或 者阻塞,直到线程A释放这个锁,如果线程A不释放这个锁,那么线程B 将永远等待下去。

​ Java中每个对象都可以用作锁,这些锁称为内置锁。线程进入同 步代码块或方法时会自动获得该锁,在退出同步代码块或方法时会释 放该锁。获得内置锁的唯一途径就是进入这个锁保护的同步代码块或 方法。

Java对象结构

Java对象(Object实例)结构包括三部分:对象头、对象体和对齐字节。

结构如下:

在这里插入图片描述

对象头

​ 对象头包括三个字段,第一个字段叫作Mark Word(标记字),用于存储自身运行时的数据,例如GC标志位、哈希码、锁状态信息等。

​ 第二个字段叫作Class Pointer(类对象指针),用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例。

​ 第三个字段叫作Array Length(数组长度)。如果对象是一个Java数组,那这个字段必须要有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。

对象体

​ 对象体包含对象的实例变量(成员变量),用于成员属性值,包括父类的成员属性值。这部分内存按4字节对齐。对象体是对象的主体部分,占用的内存空间大小取决于对象的属性数量和类型。

对齐字节

​ 对齐字节也叫作填充对齐,其作用是用来保证Java对象所占内存 字节数为8的倍数HotSpot VM的内存管理要求对象起始地址必须是8字 节的整数倍。对象头本身是8的倍数,当对象的实例变量数据不是8的 倍数时,便需要填充数据来保证8字节的对齐。

​ 对齐字节并不是必然存在的,也没有特别的含义,它仅仅起 着占位符的作用。当对象实例数据部分没有对齐(8字节的整数倍) 时,就需要通过对齐填充来补全。

Mark Word的结构信息

​ Java内置锁涉及很多重要信息,这些都存放在对象头的Mark Word字段中。Mark Word不会受到Oop指针压缩选项的影响。Java内置锁的状态总共有4种,级别由低到高依次为:无锁、偏向锁、轻量级锁和重量级锁。在JDK1.6之前只有重量级锁,之后才引入偏向锁和轻量级锁。4种锁状态会随着竞争的情况逐渐升级,而且不可逆,即只能进行锁升级,不会发生锁降级。

不同锁状态下32位Mark Work的结构信息:

在这里插入图片描述

不同锁状态下64位Mark Work的结构信息:

在这里插入图片描述

64位Mark Word的构成
  1. lock:锁状态标记位,占两个二进制位。该标记的 值不同,整个Mark Word表示的含义就不同。
  2. biased_lock:对象是否启用偏向锁标记,只占1个二进制 位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
  3. age:4位的Java对象分代年龄。在GC中,对象在Survivor区 复制一次,年龄就增加1。当对象达到设定的阈值时,将会晋升到老年 代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由 于age只有4位,因此最大值为15,这就是-XX:MaxTenuringThreshold 选项最大值为15的原因。
  4. identity_hashcode:31位的对象标识HashCode(哈希码) 采用延迟加载技术,当调用Object.hashCode()方法或者 System.identityHashCode()方法计算对象的HashCode后,其结果将被 写到该对象头中。当对象被锁定时,该值会移动到Monitor(监视器) 中。
  5. thread:54位的线程ID值为持有偏向锁的线程ID。
  6. epoch:偏向时间戳。
  7. ptr_to_lock_record:占62位,在轻量级锁的状态下指向栈帧中锁记录的指针。
  8. ptr_to_heavyweight_monitor:占62位,在重量级锁的状态下指向对象监视器的指针。

32位的Mark Word与64位的Mark Word结构相似

偏向锁

​ 如果一个同步块(或方法)没有多个线程竞争, 而且总是由同一个线程多次重入获取锁,如果每次还有阻塞线程,唤 醒CPU从用户态转为核心态,那么对于CPU是一种资源的浪费,为了解 决这类问题,就引入了偏向锁的概念。偏向锁主要解决无竞争下的锁性能问题,所谓的偏向就是偏心, 即锁会偏向于当前已经占有锁的线程。

​ 原理:如果不存在线程竞争的一个线程获得了锁,那么锁就进入偏向状态,此时Mark Word的结构变为偏向锁结构, 锁对象的锁标志位(lock)被改为01,偏向标志位(biased_lock)被改为1,然后线程的ID记录在锁对象的Mark Word中(使用CAS操作完 成)。以后该线程获取锁时判断一下线程ID和标志位,就可以直接进 入同步块,连CAS操作都不需要,这样就省去了大量有关锁申请的操作,从而也就提升了程序的性能。

偏向锁的设置

​ 如果开启了偏向锁(默认开启),那么对象创建后,markword值为0x05即最后3位为101,这时它的thread、epoch、age都为0;如果没有开启偏向锁,那么对象创建后,markword值为0x01即最后3位位001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值。使用-XX:-UseBiasedLocking可以禁用偏向锁。

​ 偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0 来禁用延迟。

偏向锁的重偏向

​ 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T 1 的对象仍有机会重新偏向T 2 ,重偏向会重置对象的 Thread ID。当撤销偏向锁阈值超过20次后,(从第二十次开始)jvm之后在给这些对象加锁时重新加偏向锁至新的线程。

偏向锁的撤销

偏向锁的撤销过程:

  1. 在一个安全点停止拥有锁的线程。
  2. 遍历线程的栈帧,检查是否存在锁记录。如果存在锁记录, 就需要清空锁记录,使其变成无锁状态,并修复锁记录指向的Mark Word,清除其线程ID。
  3. 将当前锁升级成轻量级锁。
  4. 唤醒当前线程。

​ 所以,如果某些临界区存在两个及两个以上的线程竞争,那么偏 向锁反而会降低性能。在这种情况下,可以在启动JVM时就把偏向锁的 默认功能关闭。

撤销偏向锁的情况:

  1. 多个线程竞争偏向锁。
  2. 调用偏向锁对象的hashcode()方法或者 System.identityHashCode()方法计算对象的HashCode之后,将哈希码 放置到Mark Word中,内置锁变成无锁状态,偏向锁将被撤销。
  3. 调用 wait/notify,这个只有重量级锁才有

当撤销偏向锁阈值超过40次后,(从第四十次开始)jvm会将整个类的所有对象都变为不可偏向的,新建的对象也会是不可偏向的。

偏向锁的膨胀

​ 如果偏向锁被占据,一旦有第二个线程争抢这个对象,因为偏向 锁不会主动释放,所以第二个线程可以看到内置锁偏向状态,这时表 明在这个对象锁上已经存在竞争了。JVM检查原来持有该对象锁的占有 线程是否依然存活,如果挂了,就可以将对象变为无锁状态,然后进 行重新偏向,偏向为抢锁线程。

​ 如果JVM检查到原来的线程依然存活,就进一步检查占有线程的调 用堆栈是否通过锁记录持有偏向锁。如果存在锁记录,就表明原来的 线程还在使用偏向锁,发生锁竞争,撤销原来的偏向锁,将偏向锁膨胀(INFLATING)为轻量级锁。

轻量级锁

使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁的本意是为了减少多线程进入操作系统底层的互斥锁 (Mutex Lock)的概率,并不是要替代操作系统互斥锁。所以,在争 用激烈的场景下,轻量级锁会膨胀为基于操作系统内核互斥锁实现的 重量级锁。

执行过程

(1)在抢锁线程进入临界区之前,如果内置锁 (临界区的同步对象)没有被锁定,JVM首先将在抢锁线程的栈帧中建 立一个锁记录(Lock Record),用于存储对象目前Mark Word的拷 贝,这时的线程堆栈与内置锁对象头大致如图所示,

在这里插入图片描述

(2)然后抢锁线程将使用CAS自旋操作,尝试将内置锁对象头的Mark Word的ptr_to_lock_record(锁记录指针)更新为抢锁线程栈帧中锁 记录的地址,如果这个更新执行成功了,这个线程就拥有了这个对象锁。然后JVM将Mark Word中的lock标记位改为00(轻量级锁标志), 即表示该对象处于轻量级锁状态。

(3)抢锁成功之后,JVM会将Mark Word 中原来的锁对象信息(如哈希码等)保存在抢锁线程锁记录的 Displaced Mark Word(可以理解为放错地方的Mark Word)字段中, 再将抢锁线程中锁记录的owner指针指向锁对象。

在轻量级锁抢占成功之后,锁记录和对象头的状态如图所示,

在这里插入图片描述

如果cas失败,有两种情况:

  1. 如果是其他线程已经持有了该Object的轻量级锁,这时表明有就竞争,进入锁膨胀过程
  2. 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

当退出 synchronized 代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。

当退出 synchronized 代码块(解锁时)锁记录的值不为null,这时使用 cas 将 Mark Word 的值恢复给对象头,成功则解锁成功,失败说明轻量级锁已经进行了锁膨胀或者已经升级为重量级锁,进入重量级锁的解锁流程。

轻量级锁的分类

轻量级锁主要有两种:普通自旋锁和自适应自旋锁。

普通自旋锁

​ 普通自旋锁指当有线程来竞争锁时,抢锁线程会在原 地循环等待,而不是被阻塞,直到那个占有锁的线程释放锁之后,这 个抢锁线程才可以获得锁。

​ 锁在原地循环等待的时候是会消耗CPU的,就相当于在执行一个 什么也不干的空循环。所以轻量级锁适用于临界区代码耗时很短的场 景,这样线程在原地等待很短的时间就能够获得锁了。默认情况下,自旋的次数为10次,用户可以通过XX:PreBlockSpin选项来进行更改。

自适应自旋锁

​ 自适应自旋锁就是等待线程空循环的自旋次数并非是固定 的,而是会动态地根据实际情况来改变自旋等待的次数,自旋次数由 前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

  • 如果抢锁线程在同一个锁对象上之前成功获得过锁,JVM就 会认为这次自旋很有可能再次成功,因此允许自旋等待持续相对更长 的时间。
  • 如果对于某个锁,抢锁线程很少成功获得过,那么JVM将可 能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

JDK 1.6的轻量级锁使用的是普通自旋锁,且需要使用XX:+UseSpinning选项手工开启。JDK 1.7后,轻量级锁使用自适应自旋 锁,JVM启动时自动开启,且自旋时间由JVM自动控制。

重量级锁

​ JVM中每个对象都会有一个监视器,监视器和对象一起创建、销 毁。监视器相当于一个用来监视这些线程进入的特殊房间,其义务是 保证(同一时间)只有一个线程可以访问被保护的临界区代码块。本质上,监视器是一种同步工具,也可以说是一种同步机制,监听器主要有两个特点。

  • 同步。监视器所保护的临界区代码是互斥地执行的。一个监 视器是一个运行许可,任一线程进入临界区代码都需要获得这个许 可,离开时把许可归还。
  • 协作。监视器提供Signal机制,允许正持有许可的线程暂时 放弃许可进入阻塞等待状态,等待其他线程发送Signal去唤醒;其他 拥有许可的线程可以发送Signal,唤醒正在阻塞等待的线程,让它可 以重新获得许可并启动执行。

在Hotspot虚拟机中,监视器是由C++类ObjectMonitor实现的,ObjectMonitor的Owner(_owner)、WaitSet(_WaitSet)、 Cxq(_cxq)、EntryList(_EntryList)这几个属性比较关键。ObjectMonitor的WaitSet、Cxq、EntryList这三个队列存放抢夺重量 级锁的线程,而ObjectMonitor的Owner所指向的线程即为获得锁的线 程。

  • Cxq:竞争队列(Contention Queue),所有请求锁的线程 首先被放在这个竞争队列中。
  • EntryList:Cxq中那些有资格成为候选资源的线程被移动到 EntryList中。
  • WaitSet:某个拥有ObjectMonitor的线程在调用 Object.wait()方法之后将被阻塞,然后该线程将被放置在WaitSet链 表中。

ObjectMonitor的内部抢锁过程如图所示:

在这里插入图片描述

(1) Cxq

​ Cxq并不是一个真正的队列,而是一个由Node及其next指针逻辑构成的虚拟队列。每 次新加入Node会在Cxq的队头进行,通过CAS改变第一个节点的指针为 新增节点,同时设置新增节点的next指向后续节点;从Cxq取得元素 时,会从队尾获取。Cxq结构是一个无锁结构。

​ 在线程进入Cxq前,抢锁线程会先尝试通过CAS自旋获取锁,如果 获取不到,就进入Cxq队列,这明显对于已经进入Cxq队列的线程是不 公平的。因此,synchronized同步块所使用的重量级锁是不公平锁。

(2) EntryList

​ EntryList与Cxq在逻辑上都属于等待队列。Cxq会被线程并发访 问,为了降低对Cxq队尾的争用,而建立EntryList。在Owner线程释放 锁时,JVM会从Cxq中迁移线程到EntryList,并会指定EntryList中的 某个线程(一般为Head)为OnDeck Thread(Ready Thread)。 EntryList中的线程作为候选竞争线程而存在。

(3) OnDeck Thread与Owner Thread

​ JVM不直接把锁传递给Owner Thread,而是把锁竞争的权利交给 OnDeck Thread,OnDeck需要重新竞争锁。这样虽然牺牲了一些公平 性,但是能极大地提升系统的吞吐量,在JVM中,也把这种选择行为称 为“竞争切换”。

​ OnDeck Thread获取到锁资源后会变为Owner Thread。无法获得锁 的OnDeck Thread则会依然留在EntryList中,考虑到公平性,OnDeck Thread在EntryList中的位置不发生变化(依然在队头)。

​ 在OnDeck Thread成为Owner的过程中,还有一个不公平的事情, 就是后来的新抢锁线程可能直接通过CAS自旋成为Owner而抢到锁。

(4) WaitSet

​ 如果Owner线程被Object.wait()方法阻塞,就转移到WaitSet队列 中,直到某个时刻通过Object.notify()或者Object.notifyAll()唤 醒,该线程就会重新进入EntryList中。

偏向锁、轻量级锁与重量级锁的对比

synchronized的执行过程:

  1. 线程抢锁时,JVM首先检测内置锁对象Mark Word中的 biased_lock(偏向锁标识)是否设置成1,lock(锁标志位)是否为 01,如果都满足,确认内置锁对象为可偏向状态。
  2. 在内置锁对象确认为可偏向状态之后,JVM检查Mark Word中 的线程ID是否为抢锁线程ID,如果是,就表示抢锁线程处于偏向锁状 态,抢锁线程快速获得锁,开始执行临界区代码。
  3. 如果Mark Word中的线程ID并未指向抢锁线程,就通过CAS操 作竞争锁。如果竞争成功,就将Mark Word中的线程ID设置为抢锁线 程,偏向标志位设置为1,锁标志位设置为01,然后执行临界区代码, 此时内置锁对象处于偏向锁状态。
  4. 如果CAS操作竞争失败,就说明发生了竞争,撤销偏向锁, 进而升级为轻量级锁。
  5. JVM使用CAS将锁对象的Mark Word替换为抢锁线程的锁记录 指针,如果成功,抢锁线程就获得锁。如果替换失败,就表示其他线 程竞争锁,JVM尝试使用CAS自旋替换抢锁线程的锁记录指针,如果自 旋成功(抢锁成功),那么锁对象依然处于轻量级锁状态。
  6. 如果JVM的CAS替换锁记录指针自旋失败,轻量级锁就膨胀为 重量级锁,后面等待锁的线程也要进入阻塞状态。

总体来说,偏向锁是在没有发生锁争用的情况下使用的;一旦有 了第二个线程争用锁,偏向锁就会升级为轻量级锁;如果锁争用很激 烈,轻量级锁的CAS自旋到达阈值后,轻量级锁就会升级为重量级锁。

三种内置锁的对比如图:

在这里插入图片描述


http://www.niftyadmin.cn/n/284015.html

相关文章

Web2与Web3开发的不同之处

Web2是引入交互功能的第二代互联网,也是我们今天所熟悉的。随着Web的不断发展,第三代互联网,也被称为Web3,正处于积极开发中。Web3引入了在区块链上运行的去中心化和无需许可的系统。但是Web2和Web3开发之间有什么区别呢&#xff…

LeetCode-1003. 检查替换后的词是否有效

题目链接 LeetCode-1003. 检查替换后的词是否有效 题目描述 题解 题解一(Java) 作者:仲景 题挺难懂的,很绕,然后读懂了就很简单了 就是说本来是一个字符串s,abc三个字符可以随便放在s原本字符串的左边或…

chatGPT国内可用镜像源地址

chatGPT国内可用镜像源地址 彷丶徨丶 关注 IP属地: 湖北 0.811 2023.03.15 16:02:16 字数 1,152 阅读 249,582 如果你正在尝试访问Chatgpt网站,但由于某些原因无法访问该网站,那么你可以尝试使用Chatgpt的国内镜像网站。以下是一些Chatgpt国内镜像网站的…

第二十九章 弹城简史2

第二件好运是,有个名叫莱特瑞特的小女孩发现了一种细藤,当时她在森林里被蟒蛇追赶,把鞋给跳掉了一只,迫于无奈,她用这种细藤为自己编织了一双凉鞋。 奇迹就这样发生了,她突然发现,一旦穿上这双细…

Python(一) 基础二(语句、文件读写)

1.语句 1.1.if…elif…else 类似于java的if…else if…else语句 1.1.1.判断条件 比较运算符: 、>、<、<、>、!、is、is not、in、not in 1.1.2.和is的区别 list_1 [aaa, bbb] list_2 [aaa, bbb] print(list_1 list_2) #结果:True print(list_1 is list_2)…

自动驾驶—连续系统LQR最优控制的黎卡提方程推导

1. Why use the Riccati equation? 最优控制算法LQR是Linear Quadratic Regulator的缩写,Q、R就是需要设计的半正定矩阵和正定矩阵。考虑根据实车的情况去标定此参数,从理论和工程层面去理解,如果增大Q、减小R,则此时控制系统响应速度比较快速(比较剧烈),直观反映方向…

【Fluent】接着上一次计算的结果继续计算,利用计算过程中得到的物理场(温度、速度、压力等)插值Interpolate文件初始化模型的方法

一、问题背景 因为fluent中支持的初始化无非三种类型。 1、Standard initialization 标准初始化 2、Hybridinitialization 混合初始化 3、FMG initialization FMG初始化 另外&#xff0c;还可以用UDF通过坐标判断的方式予以初始化。 但是这些初始化方法都没办法利用以前计算过…

基于c++,读取windows系统的磁盘信息

头文件 #pragma once #include <iostream> #include <Windows.h> #include <vector> #include <string> using namespace std; int getmemoryInfo(); cpp文件 #include "memory.h" int getmemoryInfo() { char lpBuffer[100] { 0 }; // 缓…