From 46759bc04450ed868d76511b63f35c9639eec5a9 Mon Sep 17 00:00:00 2001 From: 2293736867 <2293736867@qq.com> Date: Sat, 1 May 2021 06:00:41 +0800 Subject: [PATCH] JVM Chapter6 --- JVM/Chapter6/README.md | 354 +++++++++++++++++++++++++++++++++++++++++ JVM/Chapter6/pic.odg | Bin 0 -> 11224 bytes 2 files changed, 354 insertions(+) create mode 100644 JVM/Chapter6/README.md create mode 100644 JVM/Chapter6/pic.odg diff --git a/JVM/Chapter6/README.md b/JVM/Chapter6/README.md new file mode 100644 index 0000000..ea478e7 --- /dev/null +++ b/JVM/Chapter6/README.md @@ -0,0 +1,354 @@ +# Table of Contents + +* [1 来源](#1-来源) +* [2 概述](#2-概述) +* [3 对象头](#3-对象头) +* [4 锁的运行时优化](#4-锁的运行时优化) + * [4.1 偏向锁(`JDK15`默认关闭)](#41-偏向锁jdk15默认关闭) + * [4.1.1 简介](#411-简介) + * [4.1.2 加锁流程](#412-加锁流程) + * [4.1.3 例子](#413-例子) + * [4.2 轻量级锁](#42-轻量级锁) + * [4.2.1 简介](#421-简介) + * [4.2.2 加锁流程](#422-加锁流程) + * [4.3 重量级锁](#43-重量级锁) + * [4.3.1 简介](#431-简介) + * [4.3.2 加锁流程](#432-加锁流程) + * [4.4 自旋锁](#44-自旋锁) + * [4.5 锁消除](#45-锁消除) + * [4.5.1 简介](#451-简介) + * [4.5.2 例子](#452-例子) +* [5 锁的应用层优化](#5-锁的应用层优化) + * [5.1 减少持有时间](#51-减少持有时间) + * [5.2 减小粒度](#52-减小粒度) + * [5.3 锁分离](#53-锁分离) + * [5.4 锁粗化](#54-锁粗化) +* [6 无锁:`CAS`](#6-无锁cas) +* [7 参考](#7-参考) + + +# 1 来源 +- 来源:《Java虚拟机 JVM故障诊断与性能优化》——葛一鸣 +- 章节:第八章 + +本文是第八章的一些笔记整理。 + +# 2 概述 +本文主要讲述了`JVM`在运行层面和代码层面的锁优化策略,最后介绍了实现无锁的其中一种方法`CAS`。 + +# 3 对象头 +`JVM`中每个对象都有一个对象头,用于保存对象的系统信息,`64bit JVM`的对象头结构如下图所示: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210429104615245.png) + +其中: + +- `Mark Word`由`64bit`组成,一个功能数据区,可以存放对象的哈希、对象年龄、锁的指针等信息 +- `KClass Word`在没有开启指针压缩的情况下,`64bit`组成,但是`64bit JVM`会默认开启指针压缩(`+UseCompressedOops`),所以会压缩到`32bit` + +另外,从图中可以看到,不同的锁对应于不同的`Mark Word`: + +- 无锁:`25bit`空+`31bit`哈希值+`1bit`空+`4bit`分代年龄+`1bit`是否偏向锁+`2bit`锁标记 +- 偏向锁:`54bit`持有偏向锁的线程`ID`+`2bit`偏向时间戳+`1bit`空+`4bit`分代年龄+`1bit`是否偏向锁+`2bit`锁标记 +- 轻量锁:`62bit`栈中锁记录指针+`2bit`锁标记 +- 重量锁:`62bit`重量级锁指针+`2bit`锁标记 + +`JVM`如何区分锁主要看两个字段:`biased_lock`与`lock`,对应关系如下: + +- `biased_lock=0 lock=00`:轻量级锁 +- `biased_lock=0 lock=01`:无锁 +- `biased_lock=0 lock=10`:重量级锁 +- `biased_lock=0 lock=11`:`GC`标记 +- `biased_lock=1 lock=01`:偏向锁 + +# 4 锁的运行时优化 +很多时候`JVM`都会对线程竞争的操作在`JVM`层面进行优化,尽可能解决竞争问题,也会试图消除不必要的竞争,实现的方法包括: + +- 偏向锁 +- 轻量级锁 +- 重量级锁 +- 自旋锁 +- 锁消除 + +## 4.1 偏向锁(`JDK15`默认关闭) +### 4.1.1 简介 +偏向锁是`JDK 1.6`提出的一种锁优化方式,核心思想是,如果线程没有竞争,则取消已经取得锁的线程同步操作,也就是说,某个线程获取到锁后,锁就会进入偏向模式,当线程再次请求该锁时,无需再次进行相关的同步操作,从而节省操作时间。而在此期间如果有其他线程进行了锁请求,则锁退出偏向模式。 + +开启偏向锁的参数是`-XX:+UseBiasedLocking`,处于偏向锁时,`Mark Word`会记录获得锁的线程(`54bit`),通过该信息可以判断当前线程是否持有偏向锁。 + +注意`JDK15`后默认关闭了偏向锁以及禁用了相关选项,可以参考[JDK-8231264](https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8231264)。 + +### 4.1.2 加锁流程 +偏向锁的加锁过程如下: + +- 第一步:访问`Mark Word`中的`biased_lock`是否设置为`1`,`lock`是否设置为`01`,确认为可偏向状态,如果`biased_lock`为`0`,则是无锁状态,直接通过`CAS`操作竞争锁,如果失败,执行第四步 +- 第二步:如果为可偏向状态,测试线程`ID`是否指向当前线程,如果是,到达第五步,否则到达第三步 +- 第三步:如果线程`ID`没有指向当前线程,通过`CAS`操作竞争锁,如果成功,将`Mark Word`中的线程`ID`设置为当前线程`ID`,然后执行第五步,如果失败,执行第四步 +- 第四步:如果`CAS`获取偏向锁失败,表示有竞争,开始锁撤销 +- 第五步:执行同步代码 + +### 4.1.3 例子 +下面是一个简单的例子: +```java +public class Main { + private static List list = new Vector<>(); + public static void main(String[] args){ + long start = System.nanoTime(); + for (int i = 0; i < 1_0000_0000; i++) { + list.add(i); + } + long end = System.nanoTime(); + System.out.println(end-start); + } +} +``` +`Vector`的`add`是一个`synchronized`方法,使用如下参数测试: +```bash +-XX:BiasedLockingStartupDelay=0 # 偏向锁启动时间,设置为0表示立即启动 +-XX:+UseBiasedLocking # 开启偏向锁 +``` +输出如下: +```bash +1664109780 +``` +而将偏向锁关闭: +```bash +-XX:BiasedLockingStartupDelay=0 +-XX:-UseBiasedLocking +``` +输出如下: +```bash +2505048191 +``` +可以看到偏向锁还是对系统性能有一定帮助的,但是需要注意偏向锁在锁竞争激烈的场合没有太强的优化效果,因为大量的竞争会导致持有锁的线程不停地切换,锁很难一直保持在偏向模式,这样不仅仅不能优化性能,反而因为频繁切换而导致性能下降,因此竞争激烈的场合可以尝试使用`-XX:-UseBiasedLocking`禁用偏向锁。 + +## 4.2 轻量级锁 +### 4.2.1 简介 +如果偏向锁失败,那么`JVM`会让线程申请轻量级锁。轻量级锁在内部使用一个`BasicObjectLock`的对象实现,该对象内部由: + +- 一个`BasicLock`对象 +- 一个持有该锁的`Java`对象指针 + +组成。`BasicObjectLock`对象放置在`Java`栈的栈帧中,在`BasicLock`对象还会维护一个叫`displaced_header`的字段,用于备份对象头部的`Mark Word`。 + +### 4.2.2 加锁流程 +- 第一步:通过`Mark Word`判断是否无锁(`biased_lock`是否为`0`且`lock`为`01`),如果是无锁,会创建一个叫锁记录(`Lock Record`)的空间,用于存储当前`Mark Word`的拷贝 +- 第二步:将对象头的`Mark Word`复制到锁记录中 +- 第三步:拷贝成功后,使用`CAS`操作尝试将锁对象`Mark Word`更新为指向锁记录的指针,并将线程栈帧中的锁记录的`owner`指向`Object`的`Mark Word` +- 第四步:如果操作成功,那么就成功拥有了锁 +- 第五步:如果操作失败,`JVM`会检查`Mark Word`是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,就可以直接进入同步块继续执行,否则会让当前线程尝试自旋获取锁,自旋到达一定次数后如果还没有获得锁,那么会膨胀为重量级锁 + +## 4.3 重量级锁 +### 4.3.1 简介 +当轻量级锁自旋一定次数后还是无法获取锁,就会膨胀为重量级锁。相比起轻量级锁,`Mak Word`存放的是指向锁记录的指针,重量级锁中的`Mark Word`存放的是指向`Object Monitor`的指针,如下图所示: + +![在这里插入图片描述](https://img-blog.csdnimg.cn/20210430103445676.png) + +(图源见文末) + +因为锁记录是线程私有的,不能满足多线程都能访问的需求,因此重量级锁中引入了能线程共享的`ObjectMonitor`。 + +### 4.3.2 加锁流程 +初次尝试加锁时,会先`CAS`尝试修改`ObjectMonitor`的`_owner`字段,结果如下: + +- 第一种:锁没有其他线程占用,成功获取锁 +- 第二种:锁被其他线程占用,则当前线程重入锁,获取成功 +- 第三种:锁被锁记录占用,而锁记录是线程私有的,也就是属于当前线程的,这样就属于重入,重入次数为1 +- 第四种:都不满足,再次尝试加锁(调用`EnterI()`) + +而再次尝试加锁的过程,是一个循环,不断尝试获取锁直到成功为止,流程简述如下: + +- 多次尝试获取锁 +- 获取失败把线程包装后放进阻塞队列 +- 再次尝试获取锁 +- 失败后将自己挂起 +- 被唤醒后继续尝试获取锁 +- 成功则退出循环,否则继续 + +## 4.4 自旋锁 +自旋锁可以使线程没有取得锁时不被挂起,而是去执行一个空循环(也就是所谓的自旋),在若干个空循环后如果可以获取锁,则继续执行,如果不能,挂起当前线程。 + +使用自旋锁后,线程被挂起的概率相对减小,线程执行的连贯性相对加强,因此对于锁竞争不是很激烈、锁占用并发时间很短的并发线程具有一定的积极意义,但是,对于竞争激烈且锁占用时间长的并发线程,自旋等待后仍无法获取锁,还是会被挂起,浪费了自旋时间。 + +在`JDK1.6`中提供了`-XX:+UseSpinning`参数开启自旋锁,但是`JDK1.7`后,自旋锁参数被取消,`JVM`不再支持由用户配置自旋锁,自旋锁总是被执行,次数由`JVM`调整。 + +## 4.5 锁消除 +### 4.5.1 简介 +锁消除就是把不必要的锁给去掉,比如,在一些单线程环境下使用一些线程安全的类,比如`StringBuffer`,这样就可以基于逃逸分析技术可消除这些不必要的锁,从而提高性能。 + +### 4.5.2 例子 +```java +public class Main { + private static final int CIRCLE = 200_0000; + public static void main(String[] args){ + long start = System.nanoTime(); + for (int i = 0; i < CIRCLE; i++) { + createStringBuffer("Test",String.valueOf(i)); + } + long end = System.nanoTime(); + System.out.println(end-start); + } + + private static String createStringBuffer(String s1,String s2){ + StringBuffer sb = new StringBuffer(); + sb.append(s1); + sb.append(s2); + return sb.toString(); + } +} +``` +参数: +```bash +-XX:+DoEscapeAnalysis +-XX:-EliminateLocks +-Xcomp +-XX:-BackgroundCompilation +-XX:BiasedLockingStartupDelay=0 +``` +输出: +```bash +260642198 +``` +而开启锁消除后: +```bash +-XX:+DoEscapeAnalysis +-XX:+EliminateLocks +-Xcomp +-XX:-BackgroundCompilation +-XX:BiasedLockingStartupDelay=0 +``` +输出如下: +```bash +253101105 +``` +可以看到还是有一定性能提升的,但是提升不大。 + +# 5 锁的应用层优化 +锁的应用层优化就是在代码层面对锁进行优化,方法包括: + +- 减少持有时间 +- 减小粒度 +- 锁分离 +- 锁粗化 + +## 5.1 减少持有时间 +减少锁持有时间就是尽可能减少某个锁的占用时间,以减少线程互斥时间,比如: +```java +public synchronized void method(){ + A(); + B(); + C(); +} +``` +如果只有`B()`是同步操作,那么可以优化为在必要时进行同步,也就是在执行`B()`的时候进行同步操作: +```java +public void method(){ + A(); + synchronized(this){ + B(); + } + C(); +} +``` + +## 5.2 减小粒度 +所谓的减小锁粒度,就是指缩小锁定的对象范围,从而减小锁冲突的可能性,进而提高系统的并发能力。 + +减小粒度也是一种削弱多线程竞争的有效手段,比如典型的就是`ConcurrentHashMap`,在`JDK1.7`中的`segment`就是一个很好的例子。每次并发操作的时候只加锁某个特定的`segment`,从而提高并发性能。 + +## 5.3 锁分离 +锁分离就是将一个独占锁分成多个锁,比如`LinkedBlockingQueue`。在`take()`和`put()`操作中,使用的并不是同一个锁,而是分离成了一个`takeLock`和一个`putLock`: +```java +private final ReentrantLock takeLock; +private final ReentrantLock putLock; +``` +初始化操作如下: +```java +this.takeLock = new ReentrantLock(); +this.notEmpty = this.takeLock.newCondition(); +this.putLock = new ReentrantLock(); +``` +而`take()`和`put()`操作如下: +```java +public E take() throws InterruptedException { + takeLock.lockInterruptibly(); //不能两个线程同时take + //... + try { + //... + } finally { + takeLock.unlock(); + } + //... +} + +public void put(E e) throws InterruptedException { + //... + putLock.lockInterruptibly(); //不能两个线程同时put + try { + //... + } finally { + putLock.unlock(); + } + //... +} +``` +可以看到通过`putLock`以及`takeLock`两把锁实现了真正的取数据与写数据分离 + + +## 5.4 锁粗化 +通常情况下,为了保证多线程的有效并发,会要求每个线程持有锁的时间尽可能短,但是,如果对同一个锁不停请求,本身也会消耗资源,反而不利于性能优化,于是,在遇到一连串连续对同一个锁不断进行请求和释放的操作时,会把所有的锁操作整合成对锁的一次请求,减少对锁的请求同步次数,这个过程就叫锁粗化,比如 +```java +public void method(){ + synchronized(lock){ + A(); + } + synchronized(lock){ + B(); + } +} +``` +会被整合成如下形式: +```java +public void method(){ + synchronized(lock){ + A(); + B(); + } +} +``` +而在循环内申请锁,比如: +```java +for(int i=0;i<10;++i){ + synchronized(lock){ + } +} +``` +应将锁粗化为 +```java +synchronized(lock){ + for(int i=0;i<10;++i){ + } +} +``` + +# 6 无锁:`CAS` +毫无疑问,为了保证多线程并发的安全,使用锁是一种最直观的方式,但是,锁的竞争有可能会称为瓶颈,因此,有没有不需要锁的方式去保证数据一致性呢? + +答案是有的,就是这一小节介绍的主角:`CAS`。 + +`CAS`就是`Compare And Swap`的缩写,`CAS`包含三个参数,形式为`CAS(V,E,N)`,其中: + +- `V`表示内存地址值 +- `E`表示期望值 +- `N`表示新值 + +只有当`V`的值等于`E`的值时,才会把`V`设置为`N`,如果`V`的值和`N`的值不一样,那么表示已经有其他线程做了更新,当前线程什么也不做,最后`CAS`返回当前`V`的值。 + +`CAS`的操作是抱着乐观的态度进行的,总认为自己可以成功完成操作,当多个线程同时使用`CAS`操作同一个变量的时候,只会有一个胜出并成功更新,其他均会失败。失败的线程不会被挂起,仅被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 + +# 7 参考 +- [CSDN-java对象头信息](https://blog.csdn.net/zhaocuit/article/details/100208879) +- [JVM系列之:详解java object对象在heap中的结构](https://cloud.tencent.com/developer/article/1667980) +- [StackOverflow-What is in Java object header?](https://stackoverflow.com/questions/26357186/what-is-in-java-object-header) +- [CSDN-Java 中锁是如何一步步膨胀的(偏向锁、轻量级锁、重量级锁)](https://blog.csdn.net/weixin_44584387/article/details/104763837) +- [简书-Java Synchronized 重量级锁原理深入剖析上(互斥篇)](https://www.jianshu.com/p/8a8d2b42ddca) diff --git a/JVM/Chapter6/pic.odg b/JVM/Chapter6/pic.odg new file mode 100644 index 0000000000000000000000000000000000000000..55faee54f7de58fec6d3af7c6f4f9e2da0c69802 GIT binary patch literal 11224 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 literal 0 HcmV?d00001 -- GitLab