02锁机制篇(D2_锁优化)

目录

一、学习前言

二、阿里开发手册

三、synchronized 锁优化的背景

四、Synchronized的性能变化

1. Java5之前:用户态和内核态之间的切换

2. java6开始:优化Synchronized

五、锁升级

1. 无锁

2. 偏向锁

2.1. 前言

2.2. 什么是偏向锁

2.3. 偏向锁的工作过程

2.4. 为什么要引入偏向锁?

2.5. 什么是markword?

2.6. 偏向锁默认是打开还是关闭的?

2.7. 偏向锁的持有

2.8. 偏向锁JVM命令

2.8.1. linux命令

2.8.2. windows cmd命令

2.9. 偏向锁的升级

2.10. 偏向锁的的撤销

2.10.1. 调用对象 hashCode导致偏向锁撤销

2.10.2. 调用 wait/notify升级为重量级锁

2.11. 批量重偏向与批量撤销

2.11.1. 批量重偏向

2.11.2. 批量撤销

2.11.3. 知识小结

3. 轻量级锁(竞争不激烈)

3.1. 为什么要引入轻量级锁?

3.2. 轻量级锁的获取

3.3. 如何直接进入轻量级锁

3.4. 测试轻量级锁

3.5. 升级轻量级锁流程

3.6. 轻量级锁的核心原理

3.7. 轻量级锁什么时候升级为重量级锁?

3.8. 轻量锁与偏向锁的区别和不同

4. 重量级锁(竞争激烈)

4.1. 前言

4.2. 自旋优化TODO

5. 各种锁优缺点、synchronized锁升级和实现原理

五、JIT编译器对锁的优化

1. 锁粗化

2. 锁消除

六、锁降级

七、扩展

1. 自旋锁(持有锁的时间较短)

2. 自适应自旋锁

八、演示示例:锁优化的思路和方法

1. 减少锁持有时间

2. 减小锁粒度

3. 锁分离

4. 锁粗化

5. 锁消除


一、学习前言

高效并发是从JDK 5升级到JDK 6后一项重要的改进项,HotSpot虚拟机开发团队在这个版本上花

费了大量的资源去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock

Elimination)、锁膨胀(Lock Coarsening)、轻量级锁(Lightweight Locking)、偏向锁(Biased

Locking)等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的

执行效率,接下来就让我们看一下锁的进阶历程吧。

二、阿里开发手册

三、synchronized 锁优化的背景

用锁能够实现数据的安全性,但是会带来性能下降。

无锁能够基于线程并行提升程序性能,但是会带来安全性下降。

要在性能与安全找到平衡点:jdk6引入偏向锁、轻量级锁

四、Synchronized的性能变化

1. Java5之前:用户态和内核态之间的切换

java5以前,只有Synchronized,这个是操作系统级别的重量级操作

重量级锁,假如锁的竞争比较激烈的话,性能下降‘

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,

需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专

用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也

需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工

作。

在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底

层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒

一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步

代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,

这也是为什么早期的synchronized效率低的原因,Java 6之后,为了减少获得锁和释放锁所带来的

性能消耗,引入了轻量级锁和偏向锁

2. java6开始:优化Synchronized

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE

1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级

锁状态,这几个状态会随着竞争情况逐渐升级。

锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

这种锁升级不能降级的策略,目的是为了提高获得锁和释放锁的效率,

synchronized锁:由对象头中的Mark Word根据锁标志位的不同而被复用及锁升级策略

五、锁升级

synchronized用的锁是存在Java对象头里的Mark Word中

锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位

锁的4种状态:无锁状态、偏向锁状态、轻量级锁状态(自旋锁 自适应自旋锁)、重量级锁状态

(级别从低到高)锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程

1. 无锁

无锁就是没有锁,相对比较好理解。

maven引入JOL

<!--JOL   Java Object Layout Java对象布局-->
<dependency>
    <groupId>org.openjdk.jol</groupId>

    <artifactId>jol-core</artifactId>

    <version>0.9</version>

</dependency>
import org.openjdk.jol.info.ClassLayout;


public class MyObject {
    public static void main(String[] args) {
        Object o = new Object();

        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

运行结果:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

01 00 00 00 (00000001 00000000 00000000 00000000) (1)

00000001 00000000 00000000 00000000 二进制需要倒着看(每8个位看做一个整体) 1 2 3 4 变成 4 3 2 1

00000000 00000000 00000000 00000(001)

此时hashcode是0

因为懒加载的缘故,使用到hashcode才会初始化

import org.openjdk.jol.info.ClassLayout;

public class MyObject {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println("10进制hash码:"+o.hashCode());
        System.out.println("16进制hash码:"+Integer.toHexString(o.hashCode()));
        System.out.println("2进制hash码:"+Integer.toBinaryString(o.hashCode()));

        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}

运行结果:

10进制hash码:1265094477
16进制hash码:4b67cf4d
2进制hash码:1001011011001111100111101001101
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 4d cf 67 (00000001 01001101 11001111 01100111) (1741638913)
      4     4        (object header)                           4b 00 00 00 (01001011 00000000 00000000 00000000) (75)
      8     4        (object header)                           e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

00000001 01001101 11001111 01100111 01001011

倒着看 31位

0(1001011 01100111 11001111 01001101) 00000001

2进制hash码: 1001011011001111100111101001101

0(1001011 01100111 11001111 01001101) 00000001

去掉后8位

01001101 11001111 01100111 01001011

hashcode是31位

1001101 11001111 01100111 01001011

2. 偏向锁

2.1. 前言

偏向锁偏向于第一个获得它的线程,默认不存在锁竞争的情况下,常常是一个线程多次获得同一个

锁,重复获取同一把锁不会再进行锁的竞争,看看多线程卖票,同一个线程获得体会一下

/**
 * @description: 实现3个售票员卖出50张票的案例
 */
public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            //循环100次保证能够卖光票
            for (int i = 0; i < 100; i++) {
                ticket.saleTicket();
            }
        }, "T1").start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ticket.saleTicket();
            }
        }, "T2").start();
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                ticket.saleTicket();
            }
        }, "T3").start();
    }
}

/**
 * @description: 资源类
 */
class Ticket {
    private int count = 50;

    public synchronized void saleTicket() {
        if (count > 0) {
            count--;
            System.out.println(Thread.currentThread().getName() + "卖票成功,还剩" + count + "张票!");
        }
    }
}

运行结果:

T1卖票成功,还剩49张票!
T1卖票成功,还剩48张票!
T1卖票成功,还剩47张票!
T1卖票成功,还剩46张票!
T1卖票成功,还剩45张票!
T1卖票成功,还剩44张票!
T1卖票成功,还剩43张票!
T1卖票成功,还剩42张票!
T1卖票成功,还剩41张票!
T1卖票成功,还剩40张票!
T1卖票成功,还剩39张票!
T1卖票成功,还剩38张票!
T1卖票成功,还剩37张票!
T1卖票成功,还剩36张票!
T1卖票成功,还剩35张票!
T1卖票成功,还剩34张票!
T1卖票成功,还剩33张票!
T1卖票成功,还剩32张票!
T1卖票成功,还剩31张票!
T1卖票成功,还剩30张票!
T1卖票成功,还剩29张票!
T1卖票成功,还剩28张票!
T1卖票成功,还剩27张票!
T1卖票成功,还剩26张票!
T1卖票成功,还剩25张票!
T1卖票成功,还剩24张票!
T1卖票成功,还剩23张票!
T1卖票成功,还剩22张票!
T1卖票成功,还剩21张票!
T1卖票成功,还剩20张票!
T1卖票成功,还剩19张票!
T1卖票成功,还剩18张票!
T1卖票成功,还剩17张票!
T1卖票成功,还剩16张票!
T1卖票成功,还剩15张票!
T1卖票成功,还剩14张票!
T1卖票成功,还剩13张票!
T1卖票成功,还剩12张票!
T1卖票成功,还剩11张票!
T1卖票成功,还剩10张票!
T1卖票成功,还剩9张票!
T1卖票成功,还剩8张票!
T1卖票成功,还剩7张票!
T1卖票成功,还剩6张票!
T1卖票成功,还剩5张票!
T1卖票成功,还剩4张票!
T1卖票成功,还剩3张票!
T1卖票成功,还剩2张票!
T1卖票成功,还剩1张票!
T1卖票成功,还剩0张票!

发现全是t1卖出

这样就是偏向锁的情况

2.2. 什么是偏向锁

偏向锁跟synchronized有关系,当锁对象第一次被线程A获取的时候,会记录线程A的id,之后在没

有别的线程获取锁对象的前提下,线程A在执行这个锁对应的同步代码块时不会再进行任何同步操

作,即这个对象锁一直偏向线程A,这就是偏向锁。

比如更衣室中有很多衣柜,你在其中一个衣柜上写了你的名字,当下次要使用的时候发现衣柜上仍

然是你的名字,此时直接使用即可,这就省去了上锁和用钥匙开锁的过程。

Jdk6开始,默认开启偏向锁。当线程获取锁资源时,只有第一次使用 CAS 将线程 ID 设置到对象

的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发

生竞争,这个对象就归该线程所有。

  • 当锁释放之后,另一线程获得锁资源,则偏向锁升级为轻量级锁。
  • 若锁还未释放,另一线程就来竞争锁资源,则偏向锁直接升级为重量级锁。
  • 调用 wait/notify方法时,偏向锁直接升级为重量级锁,因为只有重量级锁才有该方法。
  • 调用hashcode方法,因为没有空间存储hashcode,偏向锁自动撤销,变为无锁状态。

2.3. 偏向锁的工作过程

锁对象第一次被线程A获取的时候,jvm利用cas操作将线程id写入到锁对象的mark word中,此时锁

会偏向线程A。当有线程B来争抢锁的时候,会先查看拥有偏向锁的线程A是否存活,如果A已经结

束或者不在同步代码块中,会将锁对象的标记改为无锁状态,然后升级为轻量级锁。

如果A仍然存活且在同步代码块中,偏向锁会升级为轻量级锁,A仍然会持有锁。

偏向锁的撤销是会耗费资源的,在jdk15中将其废弃(https://openjdk.java.net/jeps/374)

2.4. 为什么要引入偏向锁?

Hotspot 的作者经过研究发现,大多数情况下:

多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况,

为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和

栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来

加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果

测试成功,表示线程已经获得了锁。

如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向

锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向

当前线程。偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高

性能。即为了降低获取锁的代价,才引入的偏向锁。

通过CAS方式修改markword中的线程ID

2.5. 什么是markword?

HotSpot虚拟机的对象头(Object Header)分为两部分,

第一 部分用于存储对象自身的运行时数据,

如哈希码(HashCode)、GC分代年龄(Generational GC Age) 等。

这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特,官方称它为“Mark

Word”。

这部分是实现轻量级锁和偏向锁的关键。

另外一部分用于存储指向方法区对象类型数据的指针,

如果是数组对象,还会有一个额外的部分用于存储数组长度。

这些对象内存布局的详细内容,我们已经在第2章中学习过,在此不再赘述,只针对锁的角度做进

一步细化。

由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效

率,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。

它会根据对象的状态复用自己的存储空间。

例如在32位的HotSpot虚拟机中,对象未被锁定的状态下,Mark Word的32个比特空间里的25个比

特将用于存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,还有1个

比特固定为0(这表示未进入偏向模式)。

对象除了未被锁定的正 常状态外,还有轻量级锁定、重量级锁定、GC标记、可偏向等几种不同状

态,这些状态下对象头的存 储内容如表所示。

HotSpot虚拟机对象头Mark Word

2.6. 偏向锁默认是打开还是关闭的?

实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟,如有必要可以使用JVM参数来

关闭延迟,所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动。

开启偏向锁可以通过这个JVM参数:

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0,

如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁,关闭之

后程序默认会直接进入轻量级锁状态。

那么程序默认会进入轻量级锁状态,可以通过-XX:-UseBiasedLocking这个JVM参数关闭偏向锁。

2.7. 偏向锁的持有

理论落地:

在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个

占用他的线程拥有,这个线程就是锁的偏向线程。

那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。

这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次

加锁和释放锁。

而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。

如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。

以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加

锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开

销,性能极高。

假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻

量级锁,才能保证线程间公平竞争锁。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放

偏向锁的。

技术实现:

一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word

中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一

个synchronized方法时,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的

ID,无需再进入 Monitor去竞争对象了。

举例说明:

偏向锁的操作不用直接捅到操作系统,不涉及用户到内核转换,不必要直接升级为最高级,我们以

一个account对象的“对象头”为例,

假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到Mark

Word当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁(通过CAS修改对

象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程

并不会主动释放偏向锁。

这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的

线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过account对象的Mark Word判断:当

前线程ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。

由于之前没有释放锁,这里也就不需要重新加锁。 如果自始至终使用锁的线程只有一个,很明显

偏向锁几乎没有额外开销,性能极高。

结论:JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当

前锁,不用操作系统接入。

上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行。

2.8. 偏向锁JVM命令

查出BiasedLock相关的参数设置

java -XX:+PrintFlagsInitial |grep BiasedLock*
2.8.1. linux命令

2.8.2. windows cmd命令

默认偏向锁是打开的 UseBiasedLocking = true

但是BiasedLockingStartupDelay =4000 偏向锁 启动时间有延迟,

* 实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟,
* 如有必要可以使用JVM参数来关闭延迟
* 所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动。
*
* 开启偏向锁:
* -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
*
* 如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁
* 关闭偏向锁:关闭之后程序默认会直接进入------------------------------------------>>>>>>>>   轻量级锁状态。那么程序默认会进入轻量级锁状态
* -XX:-UseBiasedLocking

1使用默认设置 默认有延迟时间

import org.openjdk.jol.info.ClassLayout;

public class MyObject {
    public static void main(String[] args) {
        Object object = new Object();

        new Thread(() -> {
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }, "t1").start();

    }
}

运行结果:

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VA
### Carsim 中 `imp_my_out_d2_l` 的定义与用法 在 CarSim 软件环境中,变量名通常具有特定的意义和用途。对于 `imp_my_out_d2_l` 这一名称,可以从其命名结构推测出一些基本信息: #### 变量命名解析 - **`imp_`**: 表明这是一个导入(imported)或者内部模块相关的参数。 - **`my_out_`**: 提示这是某个自定义输出(custom output),可能由用户定义或通过脚本生成。 - **`d2_`**: 常见于表示二阶导数(second derivative)。这可能是加速度或其他物理量的变化率。 - **`l`**: 或许代表左侧(left)、纵向(longitudinal)或者其他方向性的描述。 因此,`imp_my_out_d2_l` 很有可能是一个用于存储某种二阶导数值的变量,具体来说,它可能涉及车辆运动学中的横向或纵向加速度变化[^1]。 #### 使用场景 此变量的具体含义取决于用户的建模需求以及所选的配置文件。如果是在驾驶员模型部分提到,则可以假设它是用来辅助计算路径跟踪误差、反馈控制信号或者是其他动态响应的一部分。例如,在实现基于线性动力学和多点预瞄的转向控制系统时,可能会利用此类数据来调整方向盘角度输入以匹配期望轨迹。 以下是关于如何查询并应用该变量的一些指导原则: 1. **查阅官方文档** - 打开 CarSim 用户手册,定位至“Custom Outputs”章节寻找是否有对该字段的标准解释。 2. **检查项目设置** - 如果您正在运行一个已有案例研究,请确认是否在 Model Configuration 面板下启用了对应选项卡下的相关功能开关。 3. **编写MATLAB接口代码** 下面展示了一段简单的 MATLAB 代码片段,演示怎样提取模拟过程中产生的这类中间结果以便进一步分析处理: ```matlab % 加载CarSim仿真结果 results = carsim_load('simulation_results.mat'); % 获取指定时间步上的imp_my_out_d2_l值 timeIndex = find(results.Time >= 5 & results.Time <= 10); % 时间范围筛选 outputValues = results.imp_my_out_d2_l(timeIndex); figure; plot(results.Time(timeIndex), outputValues); title('Plot of imp_my_out_d2_l Over Time'); xlabel('Time (s)'); ylabel('Value'); grid on; ``` 上述程序读取了一个名为 `'simulation_results.mat'` 的存档文件,并绘制出了选定时间段内的 `imp_my_out_d2_l` 曲线图象。 #### 注意事项 由于不同版本之间可能存在差异,建议始终参照最新版的帮助资源核实细节。另外需要注意的是,尽管这里提供了初步解读框架,但实际意义仍需结合具体的工程背景深入探讨才能得出确切结论。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodingW丨编程之路

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值