JVM学习记录-线程安全与锁优化(二)

前言

高效并发是程序员们写代码时一直所追求的,HotSpot虚拟机开发团队也为此付出了很多努力,为了在线程之间更高效地共享数据,以及解决竞争问题,HotSpot开发团队做出了各种锁的优化技术常见的有:自适应自旋锁(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。

自旋锁与自适应自旋

互斥同步对性能最大的影响是阻塞的实现,线程的挂起和恢复的操作时要消耗系统资源的,在并发时频繁的挂起和恢复线程这会给系统带来很大的压力。在许多应用中共享数据的锁定状态只会持续很短的一段时间,这段时间可能比线程的挂起和恢复的时间还短,这样切换线程的状态是很不值得的。因此虚拟机开发团队在JDK1.4.2中引入了自旋锁,在并发执行一段代码时,如果已经有线程获得锁,后面的线程不会被直接挂起,而是区执行一个空循环(自旋),在若干个空循环后,线程如果获得了锁,则继续执行,若线程依然不能获得锁,才会被挂起。自旋次数默认是10次,可以使用-XX:PreBlockSpin来更改。

JDK1.6中引入了自适应锁,意味着自旋的时间不再固定,而是有之前的自旋时间及锁的拥有者状态来决定,若上一次成功获得锁,那么这一次允许自旋更长时间,若这个线程很少获得锁,有可能就跳过自旋直接被挂起。

锁消除

锁消除指虚拟机在即时编译时,通过对运行上下文的扫描,发现一些被要求同步的代码,不可能存在共享数据竞争的锁,这个时候就需要把这些锁进行消除,这样可以节省毫无意义的请求时间。很多时候同步措施并不是开发人员手动加上的,而是JVM在运行期间转换时加上的。

如下代码:

public String concatString(String str1,String str2,String str3){
        return str1+str2+str3;}

因为String类是不可变的,每次的连接操作都是生成新的字符串,在JDK1.5之前会转换成StringBuffer对象的连续append()操作,在JDK1.5及以后的版本中会转换成Stringbuilder对象的连续append()操作。

转换后的代码如下:

public String concatString(String str1,String str2,String str3){
        StringBuilder stringBuilder = new StringBuilder();

        stringBuilder.append(str1);
        stringBuilder.append(str2);
        stringBuilder.append(str3);

        return stringBuilder.toString();
}

每一个append()方法都有一个同步块,锁的是stringBuilder对象,但是stringBuilder对象是concatString()方法的局部变量,显然不会被其他线程访问,因此可以安全的消除这里锁。

锁粗化

在编码时推荐同步块的作用范围尽量的小,这样范围小了,出现竞争时等待线程也能最快的拿到锁,但是如果频繁的加锁和解锁也是很消耗资源的,所以虚拟机开发团队对这种情况下的锁进行了粗化,就是说如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁的同步范围粗化到整个操作序列的外部。例如上面的代码中stringBuilder对象的每一个append()方法都有一个锁,虚拟机会把锁范围扩展到第一个append()操作之前直到最后一个append()操作之后。

轻量级锁

轻量级锁

轻量级锁是相对于操作系统互斥量的传统“重量”锁来说的。并不是来代替重量级锁,而是指在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

理解轻量级锁要先理解对象肉的内存布局,对象头分为两部分:

第一部分

用于存储对象自身的运行时数据,哈希码、GC分代年龄等。这部分数据的长度在32位和64位的虚拟分别为32bit和64bit,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。

第二部分

用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用户存储数组长度。

对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

轻量级锁加锁过程

  1. 在代码进入同步块的时候,如果此时同步对象没有被锁定(锁定标志位“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用户存储锁对象目前的Mark Word的拷贝。
  2. 然后虚拟机将使用CAS(Compare-And-Swap)操作尝试将对象的Mark Word更新为指向Lock Record的指针。
  3. 如果这个更新操作成功了,那么这个线程就拥有了该对象的锁,并且Mark Word的锁标志变为“00”。
  4. 如果这个更新操作失败了,虚拟机首先检查对象的Mark Word是否指向当前线程的栈帧,如果指向了说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果有两条以上的线程争用同一个锁,要膨胀为重量级锁,锁标志变为“10”,MarkWord中存储的就是指向重量级锁的指针,后面等待锁的线程也要进入阻塞状态。

轻量级锁解锁过程

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

总结轻量级锁

对于绝大部分的锁,在整个同步周期内都是不存在竞争的。但是如果存在锁竞争,那么除了互斥量开销外,还额外发生了CAS操作,会比重量级锁更慢。

偏向锁

偏向锁的目的

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

偏向锁定义

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

为什么叫偏向锁?

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

偏向锁的工作原理

假设当前虚拟机启用了偏向锁(-XX:+UseBiasedLocking),那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word中,如果CAS操作把获取到这个锁的线程ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。

自动解除偏向锁

当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据对象目前是否处于被锁定的状态,撤销偏向后恢复到未锁定(标志位“01”)或轻量级锁定(标志位“00”)的状态,后续的同步操作就如轻量级锁那样执行。

总结

其实JVM层面还有读写分离锁,以及靠开发人员的代码来实现减少锁的持有时间,这些都是在进行锁的优化。 本来想着ReentrantLockSychronized各写一个代码的例子呢,但是发现书上的例子,我运行起来成了死循环了。《深入理解Java虚拟机第二版》这本书第395页的代码例子,我的jdk是1.8版本,感兴趣的读者可以在自己的环境下试试,如果有运行着不是死循环的也可以告诉我一下。从写第一篇记录读这本书的博客到现在为止,差不多正好两个月,这本书确实是本好书,里面的知识有深度,适合已经有过一定Java基础的人看,讲解的也到位,只不过是该更新了,里面讲的还是jdk1.7的内容,但是最基础的东西还都是一样的。

还有就是要感悟一下了,看这本书的目的一开始是为了面试,但是从第二章开始看,刚开始看的时候第一次把JVM的内存结构弄明白后,心里很是激动的,(因为以前总是不知道jvm的堆是什么jvm的栈是什么? ),后来就一直坚持下来了,如果说只看一遍,我感觉这本书里的内容我是啥也记不住也看不明白,这样看明白了记录下来了,印象也很深刻,里面以前一些模棱两可的知识,也得到了确认。现在这本书挑着看(有些部分感觉有些偏冷门的内容就没看,例如:程序编译与代码优化)也算是看完了,然后这周也开始投简历找工作了,只是这个时间段已经过了金三银四了,可能不那么好找工作了,不过相信自己的努力不会白费的,加油,后续若有时间了会把落下的那几章也看完了,接着我要开启新的记录(设计模式学习记录)。

原文地址:https://www.cnblogs.com/jimoer/p/9126399.html

时间: 2024-10-08 04:53:31

JVM学习记录-线程安全与锁优化(二)的相关文章

JVM学习记录-线程安全与锁优化(一)

前言 线程:程序流执行的最小单元.线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址.文件I/O等),又可以独立调度(线程是CPU调度的基本单位). Java语言定义了5中线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,5中状态如下. 新建(New):创建后尚未启动的线程处于这种状态. 运行(Runnable):Runnable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能

Java并发编程学习:线程安全与锁优化

本文参考<深入理解java虚拟机第二版> 一.什么是线程安全? 这里我借<Java Concurrency In Practice>里面的话:当多个线程访问一个对象,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的. 我的理解:多线程访问一个对象,任何情况下,都能保持正确行为,就是对象就是安全的. 我们可以将Java语言中各种操作共享的数据分为以下5类:不可变.

jvm(13)-线程安全与锁优化(转)

0.1)本文部分文字转自“深入理解jvm”, 旨在学习 线程安全与锁优化 的基础知识: 0.2)本文知识对于理解 java并发编程非常有用,个人觉得,所以我总结的很详细: [1]概述 [2]线程安全 1)线程安全定义:当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的:(干货——线程安全定义) [2.1]java 语言中的线程安全(干货——java

深入理解Java虚拟机(第三版)-14. 线程安全与锁优化

14. 线程安全与锁优化 1. 什么是线程安全? 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替进行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那就称这个对象是线程安全的 2. Java语言中的线程安全 我们将Java语言下的线程安全分为以下五类:不可变.绝对线程安全.相对线程安全.线程兼容和线程对立. 1.不可变:不可变一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要进行任何线程安全保障措施

线程安全与锁优化

线程安全与锁优化 线程安全 java语言中的线程安全 不可变 相对线程安全 绝对线程安全 线程兼容 线程对立 线程安全的实现方法 互斥同步 非阻塞同步 无同步方案 锁优化 自旋与自适应自旋 锁消除 锁粗化 轻量级锁 偏向锁 线程安全与锁优化 线程安全 <Java Concurrency In Practice>的作者Brian Goetz对"线程安全"有一个比较恰当的定义:"当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进

JVM-并发-线程安全与锁优化

线程安全与锁优化 1.线程安全 (1)当多个线程访问一个对象时,如果不考虑这些线程在执行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象时是线程安全的. (2)Java语言中的线程安全 a)可以将Java语言中各种操作共享的数据分为5类:不可变,绝对线程安全,相对线程安全,线程兼容和线程对立 b) 在Java语言中不可变的对象一定是线程安全的,无论是对象的方法实现还是方的调用者,都不需要再采用任何的线程安全

深入理解java虚拟机-第13章-线程安全与锁优化

第十三章 线程安全与锁优化 线程安全 java语言中的线程安全 1 不可变.Immutable 的对象一定是线程安全的 2 绝对线程安全 一个类要达到不管运行时环境如何,调用者都不需要额外的同步措施,通常需要付出很大甚至是不切实际的代价,在java api中标注自己是线程安全的类,大多数都不是绝对的线程安全 3 相对线程安全 4 线程兼容  对象本身不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全使用 5 线程对立 线程安全的实现方法 1 互斥同步 Murua

Java虚拟机--线程安全和锁优化

Java虚拟机--线程安全和锁优化 线程安全 线程安全:当多线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的. Java中,线程安全体现在多个线程访问同一个共享数据,如果一段代码中根本不会与其他线程共享数据,可以说不存在线程安全的问题. 线程安全的安全程度,由强至弱排序,可以分为以下5类. 不可变 不可变的对象一定是线程安全的,final关键字可以实现

一夜搞懂 | JVM 线程安全与锁优化

前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习内存模型与线程? 之前我们学习了内存模型和线程,了解了 JMM 和线程,初步探究了 JVM 怎么实现并发,而本篇文章,我们的关注点是 JVM 如何实现高效 并发编程的目的是为了让程序运行得更快,提高程序的响应速度,虽然我们希望通过多线程执行任务让程序运行得更快,但是同时也会面临非常多的挑战,比如像线程安全问题.线程上下文切换的问题.硬件和软件资源限制等问题,这些都是并发编程