前言

从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会根据对象的状态复用自己的存储空间。下面是对象的状态对应的对象头的存储内容表

轻量级锁工作过程

轻量级锁加锁
  1. 在代码即将进入同步块的时候,如果此同步对象没有被锁定(标志位“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
  2. 然后,虚拟机将使用CAS操作尘世把对象的Mark Word 更新为执行Lock Record 的指针。
  3. 如果这个更新操作成功了,即代表线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。
  4. 如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。

上面说了轻量级锁的加锁过程了,它的解锁过程也同样是通过CAS操作来进行的。

  1. 如果对象的Mark Word 仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Wrod和线程中复制的Displaced Mark Word替换回来。
  2. 加入能够替换,那整个同步过程就顺利完成了;
  3. 如果替换失败,则说明有其他线程尝试过滤获取该锁,就要在释放锁的同时,唤醒被挂起的线程。

轻量级锁总结:

轻量级锁能提升新恒信性能的依据是:“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”。

如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。

偏向锁

偏向锁的意义:

偏向锁的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。

如果说轻量级锁是在无竞争的情况下使用CAS操作消除同步使用的互斥量,那偏向锁就是咋无竞争的情况下把整个同步都消除掉,连CAS操作都不去做了。

偏向锁的定义:

这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程将用于不需要在进行同步。

偏向锁加锁过程

  1. 当虚拟机启动了偏向锁,那么当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。
  2. 同时使用CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中。
  3. 如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块是,虚拟机都可以不再进行任何同步操作。

偏向锁解锁过程

当出现另外一个线程区尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就按照上面介绍的轻量级锁那样去执行。

感悟:

《深入理解Java虚拟机(第三版)》这本书,差不多算是看完了,笔记也都记录下来了,还是有几章个人觉得并不重要的章节给忽略掉了,所以就没有做笔记。这次的感觉比第一次读第二版的时候有了更深的理解,也接触到了新的知识,例如以前没研究过Java9的模块化的知识。

这次陆陆续续算上在博客上做笔记也是大概用了一个多月将近2个月,感觉比读第二版的时候,速度快了些。

每次准备换工作的时候,都是第一个想起来要看一遍这本书。但是最近两年的情况好像有所不同了,现在各大互联网公司面试必备的条件就是手撕算法,所以我也深刻的意识到了,想要出去面试光看这本书是远远不够的,因此后续的时间里,我将要开启算法的学习历程了,大家也一起加油吧!

还是那句话,我们只需努力,剩下的交给时间就好了。

深入理解JVM(③)Java的锁优化的更多相关文章

  1. 使用ZooKeeper实现Java跨JVM的分布式锁(优化构思)

    一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 说明 ...

  2. 深入理解JVM—Java 6 JVM参数配置说明

    原文地址:http://yhjhappy234.blog.163.com/blog/static/316328322011119111014657/ 使用说明< xmlnamespace pre ...

  3. Java的锁优化

    高效并发是从JDK 1.5到JDK 1.6的一个重要改进,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning).锁消除(Lo ...

  4. 深入理解JVM - Java内存模型与线程 - 第十二章

    Java内存模型 主内存与工作内存 Java内存模型主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节.此处的变量(Variable)与Java编程中 ...

  5. 深入学习重点分析java基础---第一章:深入理解jvm(java虚拟机) 第一节 java内存模型及gc策略

    身为一个java程序员如果只会使用而不知原理称其为初级java程序员,知晓原理而升中级.融会贯通则为高级 作为有一个有技术追求的人,应当利用业余时间及零碎时间了解原理 近期在看深入理解java虚拟机 ...

  6. 深入理解JVM : Java垃圾收集器

    如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现. Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商.不同版本的虚拟机所提供的垃圾收集器都可能会有很大差 ...

  7. 深入理解JVM - Java内存区域与内存溢出异常 - 第二章

    一 运行时数据区域 JVM在执行Java程序的过程中会把它管理的内存划分为若干个不同的数据区域.这些区域都有各自的用途,以及创建和销毁的时间. 程序计数器 程序计数器(Program Counter ...

  8. Java 多线程 - 锁优化

    http://www.cnblogs.com/pureEve/p/6421273.html https://www.cnblogs.com/mingyao123/p/7424911.html

  9. 使用ZooKeeper实现Java跨JVM的分布式锁(读写锁)

    一.使用ZooKeeper实现Java跨JVM的分布式锁 二.使用ZooKeeper实现Java跨JVM的分布式锁(优化构思) 三.使用ZooKeeper实现Java跨JVM的分布式锁(读写锁) 读写 ...

随机推荐

  1. Python实用笔记 (9)高级特性——列表生成式

    列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式. 举个例子,要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, ...

  2. 开发中如何让本地host和代理共存?

    开发中若遇到了需要相同域名的情况,比如利用cookie共享的sso策略,可以设置本地host映射到开发服务.设置域名,生效,正常开发. 但在公司中可能是内网,请求都需要经过代理,这时候可能会发现设置h ...

  3. 《UNIX环境高级编程》(APUE) 笔记第三章 - 文件I/O

    3 - 文件I/O Github 地址 1. 文件描述符 对于内核而言,所有打开的文件都通过 文件描述符 (file descriptor) 引用.当打开一个现有文件或创建一个新文件时,内核向进程返回 ...

  4. centos7设置系统时间与网络时间同步

    Linux的时间分为System Clock(系统时间)和Real Time Clock (硬件时间,简称RTC). 系统时间:指当前Linux Kernel中的时间. 硬件时间:主板上有电池供电的时 ...

  5. 特殊方格棋盘【状压DP】

    特殊方格棋盘[状压DP] 讲真状压DP这个东西只不过是有那么亿丢丢考验心态罢了(确信) 先从板子题说起,另加一些基础知识 题目描述 在的方格棋盘上放置n 个车,某些格子不能放,求使它们不能互相攻击的方 ...

  6. 树形dp——三色二叉树

    题目描述 一棵二叉树可以按照如下规则表示成一个由0.1.2组成的字符序列,我们称之为"二叉树序列S": 0 该树没有子节点 1S1 该树有一个子节点,S1为其二叉树序列 1S1S2 ...

  7. 状压DP之炮兵阵地

    题目 原题来自:\(NOI 2001\) 司令部的将军们打算在\(N*M\) 的网格地图上部署他们的炮兵部队.一个\(N*M\)的地图由\(N\)行\(M\)列组成,地图的每一格可能是山地(用 H表示 ...

  8. ecs架构思考

    系统管理者, ecs本身要处理的是遍历, 遍历结构处理事情. 而不同的场景要处理的事务是不一样的, 所以系统是要动态增加或者减少的. 而实体代表着一个真正的对象, 对象本身是复杂的, 拥有多种属性的. ...

  9. REST,RPC和GraphQL应用场景,WebHooks、WebSocket、HTTP Streaming应用场景。

    一.请求--响应API. 请求--响应类的API的典型做法是,通过基于HTTP的Web服务器暴露一个/套接口.API定义一些端点,客户端发送数据的请求到这些端点,Web服务器处理这些请求,然后返回响应 ...

  10. Scala 面向对象(六):面向对象的特征二:继承 (一)

    1 Scala继承的基本语法 class 子类名 extends 父类名 { 类体 } class Person { var name : String = _ var age : Int = _ d ...