深入理解JVM(③)Java的锁优化
前言
从JDK5到JDK6HotSpot虚拟机开发团队花费了大量的资源实现了各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(LightEight Locking)、偏向锁(Biased Locking)等,这些技术都是胃了在线程之间更高效地共享数据及解决竞争问题,从而提供程序的执行效率。
自旋锁与自适应锁
在Java中锁起到的作用是互斥同步,而互斥同步对性的影响最大的是阻塞,阻塞是通过挂起线程和恢复线程来实现的,这个操作是很昂贵的,消耗的服务器资源比较大。针对于此虚拟机开发团队发明了自旋锁,因为在共享数据的锁定状态只会持续很短一段时间,为了这段时间去挂起和恢复线程很不值得。所以在一个线程获得锁的同时另一个线程可以先“稍等一会儿”,但并不放弃处理器执行时间,为了让线程等待,只须让线程执行一个忙循环(自旋),这就是自旋锁。
那么这个自旋锁的自旋时间多久比较合适呢?
如自旋时间太短那就起不到自旋的作用了,太长又会占用过多的处理器资源。所以在JDK1.4.2中引入自旋锁的时候,就提供了自旋次数为10默认值以及可以自行配置的参数-XXPreBlockSpin。
在JDK1.6中对自旋锁进行了优化,引入了自适应自旋。它可以根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果上一次获得了锁,那么下一次就会被认为也会获得锁,进而自旋时间会加长;如果这个锁很少被成功获得,那么有可能就直接省略掉自旋锁,避免处理器资源浪费。
锁消除
锁消除是指:虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。
锁消除是虚拟机自行判断的,开发人员,在编写代码的时候并不用刻意的去规避这些问题,因为有些同步措施都是Java本身自己实现的。
例如如下代码:
public String concatString(String str1,String str2,String str3){
return str1 + str2 + str3;
}
因为String是被final修饰的类,所以每次变动都是会产生新的String对象来进行的,因此在编译时会对String连接做自动优化。在JDK5之前会转成StringBuffer对象进行append()操作,在JDK5以后会转为StringBuilder对象进行append()操作。
这样JDK5之前编译器就会把代码变成如下形式:
public String concatString(String str1,String str2,String str3){
StringBuffer sb = new StringBuffer();
sb.append(str1);
sb.append(str2);
sb.append(str3);
return sb.toString();
}
因为StringBuffer::append()方法就涉及到同步块,锁的就是sb对象。所以发现sb的动态作用域在concatString()方法内部,其他线程又无法访问到它,因此这里的锁就可以被安全的消除。
锁粗化
我们在编写代码的时候,一般会遵循一个原则,就是尽量将同步块的作用范围限制的最小,只在共享数据的实际作用域中才进行同步,这样同步操作数量会变得更少,即使存锁竞争,等待锁的线程也能尽可能快地拿到锁。
但是实际情况,在一系列连续操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
上面的代码中concatString()方法就是频繁的堆sb对象进行加锁,虚拟机会探测到这种情况,将锁的范围扩展到整个系列操作的外部。就是在第一个append()操作之前到最后一个append()操作之后,只需要加一次锁就可以了。
总结一下锁粗化:虚拟机探测到有一系列零碎的操作都对同一个对象加锁,将会加锁的同步范围扩展(粗化)到整个系列的操作外部。
轻量级锁
轻量级锁是相对于操作系统互斥量来实现的“重量级”锁而言的,但是轻量级锁并不用来替代重量级锁的,它是指在没有多线程竞争的前提下,减少重量级锁使用操作系统互斥量产生的性能消耗。
要理解轻量级锁,必须要对虚拟机对象的内存布局(尤其是对象头部分)。
HotSpot虚拟机的对象头分为两部分:
- 第一部门用户存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等。这部分数据的长度咋32位和64位的虚拟机中分别会占用32个或64个比特,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。
- 第二部分是用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用户存储数组长度。
由于对象头信息是与对象自身定义的数据无关的额外存储成本,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。
Mark Word会根据对象的状态复用自己的存储空间。下面是对象的状态对应的对象头的存储内容表
轻量级锁工作过程
轻量级锁加锁
- 在代码即将进入同步块的时候,如果此同步对象没有被锁定(标志位“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
- 然后,虚拟机将使用CAS操作尘世把对象的Mark Word 更新为执行Lock Record 的指针。
- 如果这个更新操作成功了,即代表线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。
- 如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。
上面说了轻量级锁的加锁过程了,它的解锁过程也同样是通过CAS操作来进行的。
- 如果对象的Mark Word 仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Wrod和线程中复制的Displaced Mark Word替换回来。
- 加入能够替换,那整个同步过程就顺利完成了;
- 如果替换失败,则说明有其他线程尝试过滤获取该锁,就要在释放锁的同时,唤醒被挂起的线程。
轻量级锁总结:
轻量级锁能提升新恒信性能的依据是:“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”。
如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
偏向锁
偏向锁的意义:
偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。
如果说轻量级锁是在无竞争的情况下使用CAS操作消除同步使用的互斥量,那偏向锁就是咋无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。
偏向锁的定义:
这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程将用于不需要在进行同步。
偏向锁加锁过程
- 当虚拟机启动了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。
- 同时使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中。
- 如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块是,虚拟机都可以不再进行任何同步操作。
偏向锁解锁过程
当出现另外一个线程区尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就按照上面介绍的轻量级锁那样去执行。
感悟:
《深入理解Java虚拟机(第三版)》这本书,差不多算是看完了,笔记也都记录下来了,还是有几章个人觉得并不重要的章节给忽略掉了,所以就没有做笔记。这次的感觉比第一次读第二版的时候有了更深的理解,也接触到了新的知识,例如以前没研究过Java9的模块化的知识。
这次陆陆续续算上在博客上做笔记也是大概用了一个多月将近2个月,感觉比读第二版的时候,速度快了些。
每次准备换工作的时候,都是第一个想起来要看一遍这本书。但是最近两年的情况好像有所不同了,现在各大互联网公司面试必备的条件就是手撕算法,所以我也深刻的意识到了,想要出去面试光看这本书是远远不够的,因此后续的时间里,我将要开启算法的学习历程了,大家也一起加油吧!
还是那句话,我们只需努力,剩下的交给时间就好了。
深入理解JVM(③)Java的锁优化的更多相关文章
- 使用ZooKeeper实现Java跨JVM的分布式锁(优化构思)
一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 说明 ...
- 深入理解JVM—Java 6 JVM参数配置说明
原文地址:http://yhjhappy234.blog.163.com/blog/static/316328322011119111014657/ 使用说明< xmlnamespace pre ...
- Java的锁优化
高效并发是从JDK 1.5到JDK 1.6的一个重要改进,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning).锁消除(Lo ...
- 深入理解JVM - Java内存模型与线程 - 第十二章
Java内存模型 主内存与工作内存 Java内存模型主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节.此处的变量(Variable)与Java编程中 ...
- 深入学习重点分析java基础---第一章:深入理解jvm(java虚拟机) 第一节 java内存模型及gc策略
身为一个java程序员如果只会使用而不知原理称其为初级java程序员,知晓原理而升中级.融会贯通则为高级 作为有一个有技术追求的人,应当利用业余时间及零碎时间了解原理 近期在看深入理解java虚拟机 ...
- 深入理解JVM : Java垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现. Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商.不同版本的虚拟机所提供的垃圾收集器都可能会有很大差 ...
- 深入理解JVM - Java内存区域与内存溢出异常 - 第二章
一 运行时数据区域 JVM在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间. 程序计数器 程序计数器(Program Counter ...
- Java 多线程 - 锁优化
http://www.cnblogs.com/pureEve/p/6421273.html https://www.cnblogs.com/mingyao123/p/7424911.html
- 使用ZooKeeper实现Java跨JVM的分布式锁(读写锁)
一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 读写 ...
随机推荐
- python高阶-Linux基础命令集
声明: 1)仅作为个人学习,如有冒犯,告知速删! 2)不想误导,如有错误,不吝指教! 1: 查看文件信息:ls ls常用参数: 参数 含义 -a 显示指定目录下所有子目录与文件,包括隐藏文件 -l 以 ...
- Nginx配置upstream实现负载均衡1
如果Nginx没有仅仅只能代理一台服务器的话,那它也不可能像今天这么火,Nginx可以配置代理多台服务器,当一台服务器宕机之后,仍能保持系统可用.具体配置过程如下: 1. 在http节点下,添加ups ...
- 问题: No module named _gexf 解决方法
最近在参与一个社交网络数据可视化的项目,要在后端将社交网络信息组建成网络传至前端以使其可视化.前端使用Echart显示网络,后端要通过Python的Gexf库组建网络. Gexf库安装过程为: pip ...
- 洛谷 P2298 【Mzc和男家丁的游戏 】
这道题还是挺水的,广搜模板题,注意一下细节就是了. :码代上上代码: #include <bits/stdc++.h> using namespace std; int n , m , s ...
- Write a program that prints its input one word per line.
#include <stdio.h> #define State '\n' void main() { int Juge=;/*only one space*/ int c=; while ...
- Spring Boot注解大全,一键收藏了!
本文首发于微信公众号[猿灯塔],转载引用请说明出处 今天是猿灯塔“365天原创计划”第5天. 今天呢!灯塔君跟大家讲: Spring Boot注解大全 一.注解(annotations)列表 @Spr ...
- 【笔记】在java中String类为什么要设计成final?
部分内容转自知乎:https://www.zhihu.com/question/31345592 从自己的理解进行加工,压缩. String本质上是一个final类 public final clas ...
- Java编程技术之浅析Java容器技术
Java容器 集合是一种存储数据的容器,是Java开发中使用最频繁的对象类型之一. 或许提起Collection,都会第一时间意识到List和Set以及Map等相关关键词.因为这几乎是我们日常开发里接 ...
- Linux 字符处理之【grep】
参数: -i: 不区分大小写 -c: 统计包含匹配的行数 -n: 输出行号 -v: 反向匹配 示例文件: (example.txt) The cat's name is Tom, what's the ...
- Js 利用正则 在字符串中提取数字、替换非数字字符为指定字符串
var s ="总金额4500元"; var num= s.replace(/[^-]/ig,""); alert(num);// 上述示例会把数字匹配到直接转 ...