深入理解多线程(五)—— Java虚拟机的锁优化技术

转载自 深入理解多线程(五)—— Java虚拟机的锁优化技术

本文是《深入理解多线程》的第五篇文章,前面几篇文章中我们从synchronized的实现原理开始,一直介绍到了Monitor的实现原理。这一篇在前几篇的基础上,深入介绍一下JVM中的锁优化技术。


关于这部分知识点,笔者翻阅了很多书籍和博文,都没有找到介绍的很清楚的文章,包括我正在写的后面会发出来的锁膨胀技术。笔者也是看了很多HotSpot虚拟机的源码、阅读了很多文档资料。所以,希望读者们可以把这个系列文章好好看看,和我一起,深入理解多线程。

前情提要

通过前面几篇文章,我们已经知道:

1、同步方法通过ACC_SYNCHRONIZED关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。《深入理解多线程(一)——Synchronized的实现原理

2、同步代码块通过monitorentermonitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得所锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。《深入理解多线程(四)—— Moniter的实现原理

3、在HotSpot虚拟机中,使用oop-klass模型来表示对象。每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。《深入理解多线程(二)—— Java的对象模型

4、对象头中主要包含了GC分代年龄、锁状态标记、哈希码、epoch等信息。对象的状态一共有五种,分别是无锁态、轻量级锁、重量级锁、GC标记和偏向锁。《深入理解多线程(三)—— Java的对象头

在上一篇文章的最后,我们说过,事实上,只有在JDK1.6之前,synchronized的实现才会直接调用ObjectMonitorenterexit,这种锁被称之为重量级锁。

高效并发是从JDK 1.5 到 JDK 1.6的一个重要改进,HotSpot虚拟机开发团队在这个版本中花费了很大的精力去对Java中的锁进行优化,如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。这些技术都是为了在线程之间更高效的共享数据,以及解决竞争问题。

本文,主要先来介绍一下自旋、锁消除以及锁粗化等技术。

这里简单说明一下,本文要介绍的这几个概念,以及后面要介绍的轻量级锁和偏向锁,其实对于使用他的开发者来说是屏蔽掉了的,也就是说,作为一个Java开发,你只需要知道你想在加锁的时候使用synchronized就可以了,具体的锁的优化是虚拟机根据竞争情况自行决定的。

也就是说,在JDK 1.5 以后,我们即将介绍的这些概念,都被封装在synchronized中了。

线程状态

要想把锁说清楚,一个重要的概念不得不提,那就是线程和线程的状态。锁和线程的关系是怎样的呢,举个简单的例子你就明白了。

比如,你今天要去银行办业务,你到了银行之后,要先取一个号,然后你坐在休息区等待叫号,过段时间,广播叫到你的号码之后,会告诉你去哪个柜台办理业务,这时,你拿着你手里的号码,去到对应的柜台,找相应的柜员开始办理业务。当你办理业务的时候,这个柜台和柜台后面的柜员只能为你自己服务。当你办完业务离开之后,广播再喊其他的顾客前来办理业务。


这个例子中,每个顾客是一个线程。 

柜台前面的那把椅子,就是

柜台后面的柜员,就是共享资源。 

你发现无法直接办理业务,要取号等待的过程叫做阻塞

当你听到叫你的号码的时候,你起身去办业务,这就是唤醒。 

当你坐在椅子上开始办理业务的时候,你就获得锁。 

当你办完业务离开的时候,你就释放锁


对于线程来说,一共有五种状态,分别为:初始状态(New) 、就绪状态(Runnable) 、运行状态(Running) 、阻塞状态(Blocked) 和死亡状态(Dead) 。

在前一篇文章中,我们介绍的synchronized的实现方式中使用Monitor进行加锁,这是一种互斥锁,为了表示他对性能的影响我们称之为重量级锁。

这种互斥锁在互斥同步上对性能的影响很大,Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,因此状态转换需要花费很多的处理器时间。

就像去银行办业务的例子,当你来到银行,发现柜台前面都有人的时候,你需要取一个号,然后再去等待区等待,一直等待被叫号。这个过程是比较浪费时间的,那么有没有什么办法改进呢?

有一种比较好的设计,那就是银行提供自动取款机,当你去银行取款的时候,你不需要取号,不需要去休息区等待叫号,你只需要找到一台取款机,排在其他人后面等待取款就行了。

之所以能这样做,是因为取款的这个过程相比较之下是比较节省时间的。如果所有人去银行都只取款,或者办理业务的时间都很短的话,那也就可以不需要取号,不需要去单独的休息区,不需要听叫号,也不需要再跑到对应的柜台了。

而,在程序中,Java虚拟机的开发工程师们在分析过大量数据后发现:共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。

如果物理机上有多个处理器,可以让多个线程同时执行的话。我们就可以让后面来的线程“稍微等一下”,但是并不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个“稍微等一下”的过程就是自旋。

自旋锁在JDK 1.4中已经引入,在JDK 1.6中默认开启。

很多人在对于自旋锁的概念不清楚的时候可能会有以下疑问:这么听上去,自旋锁好像和阻塞锁没啥区别,反正都是等着嘛。

  • 对于去银行取钱的你来说,站在取款机面前等待和去休息区等待叫号有一个很大的区别:

    • 那就是如果你在休息区等待,这段时间你什么都不需要管,随意做自己的事情,等着被唤醒就行了。 

    • 如果你在取款机面前等待,那么你需要时刻关注自己前面还有没有人,因为没人会唤醒你。

    • 很明显,这种直接去取款机前面排队取款的效率是比较高。

所以呢,自旋锁和阻塞锁最大的区别就是,到底要不要放弃处理器的执行时间。对于阻塞锁和自旋锁来说,都是要等待获得共享资源。但是阻塞锁是放弃了CPU时间,进入了等待区,等待被唤醒。而自旋锁是一直“自旋”在那里,时刻的检查共享资源是否可以被访问。

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

锁消除

除了自旋锁之后,JDK中还有一种锁的优化被称之为锁消除。还拿去银行取钱的例子说。

你去银行取钱,所有情况下都需要取号,并且等待吗?其实是不用的,当银行办理业务的人不多的时候,可能根本不需要取号,直接走到柜台前面办理业务就好了。

能这么做的前提是,没有人和你抢着办业务。

上面的这种例子,在锁优化中被称作“锁消除”,是JIT编译器对内部锁的具体实现所做的一种优化。

在动态编译同步块的时候,JIT编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。

如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。

如以下代码:

public void f({
   Object hollis = new Object();
   synchronized(hollis) {
       System.out.println(hollis);
   }
}

代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成:

public void f({
   Object hollis = new Object();
   System.out.println(hollis);
}


这里,可能有读者会质疑了,代码是程序员自己写的,程序员难道没有能力判断要不要加锁吗?就像以上代码,完全没必要加锁,有经验的开发者一眼就能看的出来的。其实道理是这样,但是还是有可能有疏忽,比如我们经常在代码中使用StringBuffer作为局部变量,而StringBuffer中的append是线程安全的,有synchronized修饰的,这种情况开发者可能会忽略。这时候,JIT就可以帮忙优化,进行锁消除。

了解我的朋友都知道,一般到这个时候,我就会开始反编译,然后拿出反编译之后的代码来证明锁优化确实存在。

但是,之前很多例子之所以可以用反编译工具,是因为那些“优化”,如语法糖等,是在javac编译阶段发生的,并不是在JIT编译阶段发生的。而锁优化,是JIT编译器的功能,所以,无法使用现有的反编译工具查看具体的优化结果。(关于javac编译和JIT编译的关系和区别,我在我的知识星球中单独发了一篇文章介绍。)

但是,如果读者感兴趣,还是可以看的,只是会复杂一点,首先你要自己build一个fasttest版本的jdk,然后在使用java命令对.class文件进行执行的时候加上-XX:+PrintEliminateLocks参数。而且jdk的模式还必须是server模式。

总之,读者只需要知道,在使用synchronized的时候,如果JIT经过逃逸分析之后发现并无线程安全问题的话,就会做锁消除。锁粗化

很多人都知道,在代码中,需要加锁的时候,我们提倡尽量减小锁的粒度,这样可以避免不必要的阻塞。

这也是很多人原因是用同步代码块来代替同步方法的原因,因为往往他的粒度会更小一些,这其实是很有道理的。

还是我们去银行柜台办业务,最高效的方式是你坐在柜台前面的时候,只办和银行相关的事情。如果这个时候,你拿出手机,接打几个电话,问朋友要往哪个账户里面打钱,这就很浪费时间了。最好的做法肯定是提前准备好相关资料,在办理业务时直接办理就好了。

加锁也一样,把无关的准备工作放到锁外面,锁内部只处理和并发相关的内容。这样有助于提高效率。

那么,这和锁粗化有什么关系呢?可以说,大部分情况下,减小锁的粒度是很正确的做法,只有一种特殊的情况下,会发生一种叫做锁粗化的优化。

就像你去银行办业务,你为了减少每次办理业务的时间,你把要办的五个业务分成五次去办理,这反而适得其反了。因为这平白的增加了很多你重新取号、排队、被唤醒的时间。

如果在一段代码中连续的对同一个对象反复加锁解锁,其实是相对耗费资源的,这种情况可以适当放宽加锁的范围,减少性能消耗。

当JIT发现一系列连续的操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中的时候,会将加锁同步的范围扩散(粗化)到整个操作序列的外部。

如以下代码:

for(int i=0;i<100000;i++){  
   synchronized(this){  
       do();  
}

会被粗化成:

synchronized(this){  
   for(int i=0;i<100000;i++){  
       do();  
}

这其实和我们要求的减小锁粒度并不冲突。减小锁粒度强调的是不要在银行柜台前做准备工作以及和办理业务无关的事情。而锁粗化建议的是,同一个人,要办理多个业务的时候,可以在同一个窗口一次性办完,而不是多次取号多次办理。

总结

自Java 6/Java 7开始,Java虚拟机对内部锁的实现进行了一些优化。这些优化主要包括锁消除(Lock Elision)、锁粗化(Lock Coarsening)、偏向锁(Biased Locking)以及适应性自旋锁(Adaptive Locking)。这些优化仅在Java虚拟机server模式下起作用(即运行Java程序时我们可能需要在命令行中指定Java虚拟机参数“-server”以开启这些优化)。

本文主要介绍了自旋锁、锁粗化和锁消除的概念。在JIT编译过程中,虚拟机会根据情况使用这三种技术对锁进行优化,目的是减少锁的竞争,提升性能。

当你来到银行,办理业务的时候,你想取钱,银行工作人员了解到你要取钱之后,让你你直接站在取款机前面排队等待,并且告诉你自己时刻关注前面的排队状况。这就叫自旋。

当你来到银行,办理业务的时候,银行工作人员告诉你,由于现在办理业务的人很少,让你直接到1号柜台去办理业务。这就叫锁消除。

当你来到银行,办理业务的时候,你取了10个号,准备进行十次排队进行转账,银行工作人员了解情况之后,但是你在一次办理业务过程中进行了10次转账,办理了所有业务。这就叫锁粗化。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/329651.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Visual Studio Code 1.0正式发布

Visual Studio Code 是一个运行于 OS X&#xff0c;Windows 和 Linux 之上的&#xff0c;针对于编写现代 web 和云应用的跨平台编辑器。 这标志着 Microsoft 第一次向开发者们提供了一款真正的跨平台编辑器。虽然完整版的 Visual Studio 仍然是只能运行在 Windows 之上&#xf…

springboot使用ImportBeanDefinitionRegistrar 动态注册bean

【README】 1.采用 ImportBeanDefinitionRegistrar 动态注册bean&#xff0c;应用场景有&#xff1a; 如 一个后端服务需要用到多个 rabbitmq集群客户端&#xff0c;kafka客户端&#xff1b;这时就需要手动注册多个同类型的bean&#xff0c;但不同beanName&#xff0c;并用 …

python 线性回归函数_Python实现的简单线性回归算法实例分析

本文实例讲述了Python实现的简单线性回归算法。分享给大家供大家参考&#xff0c;具体如下&#xff1a;用python实现R的线性模型(lm)中一元线性回归的简单方法&#xff0c;使用R的women示例数据&#xff0c;R的运行结果&#xff1a;> summary(fit)Call:lm(formula weight ~…

UWP应用模型概述

Andrew Clinick是微软的一名项目经理&#xff0c;在Build 2016大会上&#xff0c;他概括地讲述了通用Windows平台&#xff08;UWP&#xff09;应用模型的新特性。今年的其中一个亮点是&#xff0c;代号为Centennial的项目实现了桌面应用程序到UWP应用的转换。 Andrew一开始就阐…

8.3-指令流水(学习笔记)

【README】 本文总结自bilibili《计算机组成原理&#xff08;哈工大刘宏伟&#xff09;》的视频讲解&#xff0c;非常棒&#xff0c;墙裂推荐&#xff1b; 【1】如何提高机器速度 1 提高访存速度多体并行&#xff1a;对多个存储体进行交叉访问&#xff0c;在一个主存周期中&am…

Java虚拟机是如何执行线程同步的

转载自 [译]Java虚拟机是如何执行线程同步的想介绍下synchronized的原理&#xff0c;但是又不知道从何下手&#xff0c;在网上看到一篇老外的文章&#xff0c;介绍了和线程同步相关的几个基础知识点。所以想把它翻译一下给大家看看。相信看过这些基础知识之后再看我后面要写的s…

python开方运算符_Pytorch Tensor基本数学运算详解

1. 加法运算示例代码&#xff1a;import torch# 这两个Tensor加减乘除会对b自动进行Broadcastinga torch.rand(3, 4)b torch.rand(4)c1 a bc2 torch.add(a, b)print(c1.shape, c2.shape)print(torch.all(torch.eq(c1, c2)))输出结果&#xff1a;torch.Size([3, 4]) torch.…

重新审视演进式设计

演进式设计是一种理念&#xff0c;它曾经颠覆过传统笨拙的计划式设计&#xff0c;如今&#xff0c;它依旧焕发着生命力&#xff0c;但我们不能以静止的眼光去看待它&#xff0c;而应该尝试着引入一些新的方法、框架乃至技术。 ♦ ♦ 说起来&#xff0c;所谓Evolutionary Design…

9.1-微操作命令的分析(学习笔记)

【README】 本文总结自bilibili《计算机组成原理&#xff08;哈工大刘宏伟&#xff09;》的视频讲解&#xff0c;非常棒&#xff0c;墙裂推荐&#xff1b; 1&#xff09;完成一条指令需要4个周期&#xff1a; 取值周期&#xff1b;间址周期&#xff08;或有&#xff09;&…

小知识 | Java中的“魔数”

转载自 小知识 | Java中的“魔数”在编程过程中&#xff0c;我们可能经常听到“魔数”这个词&#xff0c;那么这个词到底指的是什么呢&#xff1f;什么数叫做魔数呢&#xff1f;一、标识文件类型的“魔数”大多数情况下&#xff0c;我们都是通过扩展名来识别一个文件的类型的&a…

python setup.py 指定文件到指定路径_linux下python安装到指定目录

由于使用公司服务器时没有root权限&#xff0c;只能把python安装到个人文件夹下&#xff0c;使用源码包方式安装&#xff0c;这里记录一下。1.python下载cd到目录/users/w&#xff0c;在此目录下安装python。通过wget命令下载python源码包&#xff1a;2.解压python到当前目录$ …

9.2-控制单元CU的功能(学习笔记)

【README】 本文总结自bilibili《计算机组成原理&#xff08;哈工大刘宏伟&#xff09;》的视频讲解&#xff0c;非常棒&#xff0c;墙裂推荐&#xff1b; 【1】CU功能&#xff08;CPU内部不采用总线方式&#xff09; 控制单元的功能&#xff1a; CU发出各种控制命令或微指令…

WEB API 系列(二) Filter的使用以及执行顺序

在WEB Api中&#xff0c;引入了面向切面编程&#xff08;AOP&#xff09;的思想&#xff0c;在某些特定的位置可以插入特定的Filter进行过程拦截处理。引入了这一机制可以更好地践行DRY(Don’t Repeat Yourself)思想&#xff0c;通过Filter能统一地对一些通用逻辑进行处理&…

图解 SQL 里的各种 JOIN

转载自 图解 SQL 里的各种 JOIN 从业以来主要在做客户端&#xff0c;用到的数据库都是表结构比较简单的 SQLite&#xff0c;以我那还给老师一大半的 SQL 水平倒也能对付。现在偶尔需要到后台的 SQL Server 里追查一些数据问题&#xff0c;就显得有点捉襟见肘了&#xff0c;特…

python cmd闪退_使用cmd python模块时,如何使程序正常崩溃?

会发生的是,如果您的代码引发运行时异常并且您的完成不起作用,您就不知道为什么因为没有打印回溯.尝试这个非常短的代码来看看我的意思&#xff1a;程序应该在c 2“ddda”行崩溃,显然你要添加一个字符串和一个int,这根本不起作用.但是不是崩溃,而是异常被抓住了,你不知道发生了…

10.1-控制单元CU的组合逻辑设计

【README】 1.本文总结自bilibili《计算机组成原理&#xff08;哈工大刘宏伟&#xff09;》的视频讲解&#xff0c;非常棒&#xff0c;墙裂推荐&#xff1b; 2.精简指令集RISC&#xff08;简单指令集合&#xff09;采用的就是这种组合逻辑设计的硬连方式&#xff08;非常重要…

ENode 2.8 最新架构图简介

ENode架构图 什么是ENode ENode是一个.NET平台下&#xff0c;纯C#开发的&#xff0c;基于DDD,CQRS,ES,EDA,In-Memory架构风格的&#xff0c;可以帮助开发者开发高并发、高吞吐、可伸缩、可扩展的应用程序的一个应用开发框架。 开源项目地址&#xff1a;https://github.com/tang…

Hibernate注解(一)之持久化实体

翻译自 Hibernate Annotations1. 创建POJO作为持久化实体 每个持久化POJO类都是一个实体&#xff0c;并使用 Entity注解&#xff08;在类级别&#xff09;声明&#xff1a; Entity public class Flight implements Serializable {Long id;Idpublic Long getId() { return id;…

python请输入你的名字_实现《你的名字》同款滤镜,python+opencv

好久没有上简书&#xff0c;最近上来一看发现这篇文章阅读量远超了其他的文章&#xff0c;还有评论提到说在讲技术的时候卖萌不好。哈哈&#xff0c;当时写的时候完全没想过会有人搜到看&#xff0c;只是为自己的作品留个念想&#xff0c;所以文风比较散漫随意。于是这次来小修…

10.2-控制单元CU的微程序设计

【README】 1.本文总结自bilibili《计算机组成原理&#xff08;哈工大刘宏伟&#xff09;》的视频讲解&#xff0c;非常棒&#xff0c;墙裂推荐&#xff1b; 2.微指令&#xff1a;1条微指令就是多个bit位&#xff0c;如8个bit&#xff0c;每个bit位表示一种微操作&#xff1b…