JVM锁的优化与逃逸分析
锁消除
- 是指JVM即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
- 锁消除的主要判定依据来源于逃逸分析的数据支持。
- JIT(Just-In-Time,即时编译):是一种在程序运行时将部分热点代码编译成机器代码的技术,以提高程序的执行性能的机制。
将不存在并发的代码块上加的锁进行消除
逃逸分析
-
1 栈上分配对象内存:如果确定一个对像被判定为不会逃逸出方法之外,那么我们就可以在栈上分配对象,随着方法的生命周期而被回收掉,也就不需要浪费在堆中分配对象并且通过复杂的gc操作的资源了。
-
2 同步消除:线程同步本身就是个相对耗时的过程。如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,也就是说该变量不会有同步竞争,也就可以将同步操作消除掉。
-
3 标量替换:如果逃逸分析能够确定一个对象不会被外部所访问,并且这个对象可以被拆散的话(拆散成一个个的标量(int long 以及reference类型)),创建若干个被这个方法使用到的成员变量来代替
- 总结: 将原本需要分配在堆上的对象拆解成若干个基础数据类型存储在栈上,进一步减少堆空间的使用。
锁粗化
如果一系列的操作在同一个对象上反复加锁的场景下,我们可以使用锁粗化来进行优化。
如:for循环中加锁,或者StringBuffer的append操作(对StringBuffer加锁),这些场景下只加一次锁就可以了。
锁膨胀的过程
https://blog.csdn.net/fan1865221/article/details/96338419
- 在 jdk6 之后便引入了“偏向锁”和“轻量级锁”,所以总共有4种锁状态,级别由低到高依次为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
这几个状态会随着竞争情况逐渐升级,此过程为不可逆。所以 synchronized 锁膨胀过程其实就是无锁 → 偏向锁 → 轻量级锁 → 重量级锁的一个过程。
[ˈmɑːnɪtər] monitor
- 在使用 synchronized 来同步代码块的时候,编译后,会在代码块的起始位置插入 monitorenter指令,在结束或异常处插入 monitorexit指令。
当执行到 monitorenter 指令时,将会尝试获取对象所对应的 ** monitor **的所有权,即尝试获得对象的锁。而 synchronized 用的锁是存放在 Java对象头中的。
所以引出了两个关键词:“Java 对象头” 和 “Monitor”
对象头中的 Mark Word 保存着锁的标志位: 无锁:01 偏向锁:01 轻量级锁:00 重量级锁:10
- 1 无锁
- 2 偏向锁:是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。
- 当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。
轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。 - 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
- 偏向锁在 JDK 6 及之后版本的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
- 当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。
- 3 轻量级锁:引入轻量级锁的主要目的是:在多线程竞争不激烈的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
- 需要注意的是轻量级锁并不是取代重量级锁,而是在大多数情况下同步块并不会出现严重的竞争情况,所以引入轻量级锁可以减少重量级锁对线程的阻塞带来的开销。
- 所以偏向锁是认为环境中不存在竞争情况,而轻量级锁则是认为环境中不存在竞争或者竞争不激烈,所以轻量级锁一般都只会有少数几个线程竞争锁对象,
其他线程只需要稍微等待(自旋)下就可以获取锁,但是自旋次数有限制,如果自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,
- 4 重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
JVM自旋锁和自适应自旋锁优化
目的是降低线程执行行的cpu切换资源的消耗。
自旋锁
-
如果当前有多个cpu,并且存在2个或2个以上的线程同时争夺同一资源时,我们就可以让后面那个请求锁的线程不放弃 CPU 的执行时间,而是自旋等待,如果锁被释放的同事就可以获取到锁资源,避免了线程切换带来的消耗。
-
但是也是有缺点的,如果锁资源被获取的时间比较长,那么自旋带来的消耗也是比较大的,占用大量的处理器cpu的时间。
-
所以,自旋等待的时间必须要有一定的限度,如果**自旋超过了限定次数(默认是10次,可以使用 -XX:PreBlockSpin 来更改)**没有成功获得锁,就应当挂起线程。
自适应自旋锁
自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
- 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。比如:100个循环
- 如果对于某个锁,自旋很少成功获得过锁,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。