Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

2023-11-04

Java并发编程系列:

一、重量级锁

  上篇文章中向大家介绍了Synchronized的用法及其实现的原理。现在我们应该知道,Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。

二、轻量级锁 

  锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。锁的状态保存在对象的头文件中,以32位的JDK为例:

锁状态

25 bit

4bit

1bit

2bit

23bit

2bit

是否是偏向锁

锁标志位

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向互斥量(重量级锁)的指针

10

GC标记

11

偏向锁

线程ID

Epoch

对象分代年龄

1

01

无锁

对象的hashCode

对象分代年龄

0

01

  “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。

1、轻量级锁的加锁过程

  (1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图2.1所示。

  (2)拷贝对象头中的Mark Word复制到锁记录中。

  (3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。

  (4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。

  (5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。

 

                     图2.1 轻量级锁CAS操作之前堆栈与对象的状态

   

                      图2.2 轻量级锁CAS操作之后堆栈与对象的状态

2、轻量级锁的解锁过程:

  (1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。

  (2)如果替换成功,整个同步过程就完成了。

  (3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。

三、偏向锁

  引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

1、偏向锁获取过程:

  (1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。

  (2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。

  (3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。

  (4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

  (5)执行同步代码。

2、偏向锁的释放:

  偏向锁的撤销在上述第四步骤中有提到偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

3、重量级锁、轻量级锁和偏向锁之间转换

 

                                        图 2.3三者的转换图

  该图主要是对上述内容的总结,如果对上述内容有较好的了解的话,该图应该很容易看懂。

四、其他优化 

1、适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中我们知道当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

2、锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:

 1 package com.paddx.test.string;
 2 
 3 public class StringBufferTest {
 4     StringBuffer stringBuffer = new StringBuffer();
 5 
 6     public void append(){
 7         stringBuffer.append("a");
 8         stringBuffer.append("b");
 9         stringBuffer.append("c");
10     }
11 }

  这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

3、锁消除(Lock Elimination):锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:

 1 package com.paddx.test.concurrent;
 2 
 3 public class SynchronizedTest02 {
 4 
 5     public static void main(String[] args) {
 6         SynchronizedTest02 test02 = new SynchronizedTest02();
 7         //启动预热
 8         for (int i = 0; i < 10000; i++) {
 9             i++;
10         }
11         long start = System.currentTimeMillis();
12         for (int i = 0; i < 100000000; i++) {
13             test02.append("abc", "def");
14         }
15         System.out.println("Time=" + (System.currentTimeMillis() - start));
16     }
17 
18     public void append(String str1, String str2) {
19         StringBuffer sb = new StringBuffer();
20         sb.append(str1).append(str2);
21     }
22 }

虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中逃逸出去,所以其实这过程是线程安全的,可以将锁消除。下面是我本地执行的结果:

  为了尽量减少其他因素的影响,这里禁用了偏向锁(-XX:-UseBiasedLocking)。通过上面程序,可以看出消除锁以后性能还是有比较大提升的。

  注:可能JDK各个版本之间执行的结果不尽相同,我这里采用的JDK版本为1.6。

五、总结 

  本文重点介绍了JDk中采用轻量级锁和偏向锁等对Synchronized的优化,但是这两种锁也不是完全没缺点的,比如竞争比较激烈的时候,不但无法提升效率,反而会降低效率,因为多了一个锁升级的过程,这个时候就需要通过-XX:-UseBiasedLocking来禁用偏向锁。下面是这几种锁的对比:

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

如果始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。

同步块执行速度非常快。

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量。

同步块执行速度较长。

 

 参考文献:

http://www.iteye.com/topic/1018932

http://www.infoq.com/cn/articles/java-se-16-synchronized

http://frank1234.iteye.com/blog/2163142

https://www.artima.com/insidejvm/ed2/threadsynch3.html

http://www.tuicool.com/articles/2aeAZn



http://www.cnblogs.com/paddix/p/5405678.html




本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java并发编程:Synchronized底层优化(偏向锁、轻量级锁) 的相关文章

随机推荐

  • yolo5 [Errno 32] Broken pipe OSError: [WinError 1455] 页面文件太小,无法完成操作

    错误代码 File D Users Administrator miniconda3 lib runpy py line 85 in run code exec code run globals File E project weilan
  • npm安装vue

    安装vue cli 脚手架命令 npm install g vue cli 适用于vue cli 2 0 npm install g vue cli 适用于vue cli 3 0 一 初次安装 vue cli 3 0 步骤 1 全局安装vu
  • SpringBoot起飞系列-Web开发(五)

    一 前言 从今天你开始我们就开始进行我们的web开发 之前的一篇用SpringBoot起飞系列 使用idea搭建环境 二 已经说明了我们如何进行开发 当然这是搭建起步 接下来我们就开始进行详细的开发 包括springboot中各种start
  • AD(altium designer)15原理图与PCB设计教程(三)—— 原理图元器件库的管理

    目录 序言 原理图库文件编辑器 原理图库文件编辑器的启动 原理图库元器件的创建 设置工作区参数 库元器件的创建 原理图库元器件的编辑 原理图库元器件菜单命令 原理图库文件添加模型 创建含有子部件的库元器件 报表输出及库报告生成 输出报表 生
  • Android Studio 的NotePad制作(日志本),androidstudio基础

    此为点击设置之后的界面 设置颜色的代码如下 themeList ListPreference findPreference themelist themeList setSummary PrefVO themeListValue theme
  • 优化struts 2

    优化struts 2 1 关闭logging和devMode struts property struts devModel false 2 用freemarker tags 代替 struts2 tag
  • 硬件复习

    大家随便看看就好
  • Home Work--计蒜客

    临近开学了 大家都忙着收拾行李准备返校 但 I Love C 却不为此担心 因为他的心思全在暑假作业上 目前为止还未开动 暑假作业是很多张试卷 我们这些从试卷里爬出来的人都知道 卷子上的题目有选择题 填空题 简答题 证明题等 而做选择题的好
  • LeetCode-Python-54. 螺旋矩阵

    给定一个包含 m x n 个元素的矩阵 m 行 n 列 请按照顺时针螺旋顺序 返回矩阵中的所有元素 示例 1 输入 1 2 3 4 5 6 7 8 9 输出 1 2 3 6 9 8 7 4 5 示例 2 输入 1 2 3 4 5 6 7 8
  • 广州站

    12月2日 由阿里云 Serverless 团队主办的 云原生 Serverless 技术实践营 在广州顺利举行 本次活动面向所有企业技术人员 主打 沉浸式沙龙体验 6 小时搞定 Serverless 企业落地 活动当天 5 位阿里云技术讲
  • 80%白领危了!OpenAI发布GPT时代就业秘笈:34大铁饭碗保命

    导读 GPT 4发布没几天 OpenAI直接告诉所有人 GPTs是通用技术 80 的美国人的工作受到影响 想要保命 且看这34大 铁饭碗 前脚刚推出GPT 4 OpenAI后脚就发布了35页论文官宣 80 的美国人 都会受到AI的影响 研究
  • STM32“隐藏的定时器”-DWT

    01 前言 在之前的文章在 STM32延时函数的四种方法 使用定时器延时 在 如何测量代码运行时间 中提到使用定时器外设计算代码运行时间 文中提到这种方法的明显缺点就是需要占用一个定时器 一些MCU在特定应用场景下定时器外设资源是十分稀缺的
  • 信息安全大赛出的题目

    今天是信息安全大赛决赛的日期 可惜还要去市里培训 不能去现场观看 听说的做的一塌糊涂 也许是第一次举办这样的比赛 在校内还没有这样的氛围的缘故吧 中午打了电话 得知总分150左右的 最高的得了68分 我在反思 是我们把题目的难度没控制好 还
  • 华为OD机试 C++【最少步数】

    题目 你在一个一维的数轴上 起始位置为0 你每次只能走2步或3步 无论是向左还是向右 有时你可能需要走到负坐标上去 才能最终到达你的目标位置 任务 给定一个坐标点 找出到达那里的最小步数 输入 一个整数 n 表示目标坐标位置 1 lt n
  • Matter Project 入门 – Chip-tool 调试终端设备

    注意 本指南是对 Matter TE7 5 的投诉 请查看芯片工具 GitHub 页面 以获取任何最新更改 如果您没有遵循Matter 构建指南 您应该首先在您的 Raspberry Pi 4 上准备 Matter 环境 然后导航到 con
  • 二货小易有一个W*H的网格盒子,网格的行编号为0~H-1,网格的列编号为0~W-1。每个格子至多可以放一块蛋糕,任意两块蛋糕的欧几里得距离不能等于2。

    二货小易有一个W H的网格盒子 网格的行编号为 0到H 1 网格的列编号为 0到W 1 每个格子至多可以放一块蛋糕 任意两块蛋糕的欧几里得距离不能等于2 对于两个格子坐标 x1 y1 x2 y2 的欧几里得距离为 x1 x2 x1 x2 y
  • Idea: debug 跳出循环

    点击循环下面的某行代码 点击run to cusor按钮
  • RedHat Linux各版本汇总

    Linux的发行版本可以大体分为两类 一类是商业公司维护的发行版本 一类是社区组织维护的发行版本 前者以著名的Redhat RHEL 为代表 后者以Debian为代表 Redhat有两大 Linux产品系列 其一是免费的Fedora Cor
  • 关于tomcat7-maven-plugin插件的使用

    最近在学习黑马的淘淘项目 第一步是使用maven依赖进行环境搭建 其中使用到了tomcat7 maven plugin这个插件 视频中老师给的依赖大致是这样的
  • Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

    Java并发编程系列 Java 并发编程 核心理论 Java并发编程 Synchronized及其实现原理 Java并发编程 Synchronized底层优化 轻量级锁 偏向锁 Java 并发编程 线程间的协作 wait notify sl