偏向锁批量充偏向&批量撤销
当一个线程反复进入同步块,偏向锁的性能开销可以忽略,其他线程尝试获取锁,需要等到safe point
讲偏向锁撤销,转为无锁或轻量级,会消耗性能,
所以,多线程竞争激烈情况下,偏向锁就会被取消,(偏向锁的批量偏向,批量撤销)
原理
以class为单位,每个class维护一个偏向锁撤销计数器,一次该class对象发生偏向撤销,计数器+1,
当达到峰值(默认20),JVM会认为该class偏向锁有问题,就会批量重偏向
每个class对象,会有对应的epoch 字段,每个处于偏向锁状态的对象的mark word 也有,
初始值为创建该对象时的class epoch值,,每次发生批量重偏向,就会+1,同时遍历jvm所有线程的栈
找到该class所有处于加锁状态的偏向锁,讲epoch字段改为新值,
下次获取锁时:发现当前对象的epoch值与class的epoch值不等,那就算是已经偏向其他线程,也不会执行
撤销操作,而是通过cas将Mark work 的ThreadId 改为当前线程ID
当达到重偏向阈值,JVM认为该class的使用场景存在多线程竞争,会标记该class不可偏向,对于该class的锁
直接走轻量级锁的逻辑
应用场景
批量重偏向(bulk rebias): 为了解决一个线程创建大量对象并执行同步操作,后来一个线程也将这些对象
作为锁对象进程操作,导致大量的偏向锁撤销操作
批量撤销(bulk revoke):在激烈多线程竞争下不在使用偏向锁
jvm参数
-XX:+PrintFlagsFinal # 启动时即可输出JVM的默认参数值
-XX:BiasedLockingBulkRebiasThreshold #批量重偏向阈值
-XX:BiasedLockingBulkRevokeThreshold #批量撤销阈值
测试:批量重偏向
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程,重偏向会重置对象 的 Thread ID
@Slf4j
public class BiasedLockingTest {
//延时产生可偏向对象
Thread.sleep(5000);
// 创建一个list,来存放锁对象
List<Object> list = new ArrayList<>();
// 线程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建锁对象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thead1").start();
//睡眠3s钟保证线程thead1创建对象完成
Thread.sleep(3000);
log.debug("打印thead1,list中第20个对象的对象头:");
log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 线程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if(i>=15&&i<=21||i>=38){
log.debug("thread2-第" + (i + 1) + "次加锁执行中\t"+
ClassLayout.parseInstance(obj).toPrintable());
}
}
if(i==17||i==19){
log.debug("thread2-第" + (i + 1) + "次释放锁\t"+
ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thead2").start();
LockSupport.park();
}
测试:批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时
@Slf4j
public class BiasedLockingTest {
public static void main(String[] args) throws InterruptedException {
//延时产生可偏向对象
Thread.sleep(5000);
// 创建一个list,来存放锁对象
List<Object> list = new ArrayList<>();
// 线程1
new Thread(() -> {
for (int i = 0; i < 50; i++) {
// 新建锁对象
Object lock = new Object();
synchronized (lock) {
list.add(lock);
}
}
try {
//为了防止JVM线程复用,在创建完对象后,保持线程thead1状态为存活
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thead1").start();
//睡眠3s钟保证线程thead1创建对象完成
Thread.sleep(3000);
log.debug("打印thead1,list中第20个对象的对象头:");
log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
// 线程2
new Thread(() -> {
for (int i = 0; i < 40; i++) {
Object obj = list.get(i);
synchronized (obj) {
if(i>=15&&i<=21||i>=38){
log.debug("thread2-第" + (i + 1) + "次加锁执行中\t"+
ClassLayout.parseInstance(obj).toPrintable());
}
}
if(i==17||i==19){
log.debug("thread2-第" + (i + 1) + "次释放锁\t"+
ClassLayout.parseInstance(obj).toPrintable());
}
}
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thead2").start();
Thread.sleep(3000);
new Thread(() -> {
for (int i = 0; i < 50; i++) {
Object lock =list.get(i);
if(i>=17&&i<=21||i>=35&&i<=41){
log.debug("thread3-第" + (i + 1) + "次准备加锁\t"+
ClassLayout.parseInstance(lock).toPrintable());
}
synchronized (lock){
if(i>=17&&i<=21||i>=35&&i<=41){
log.debug("thread3-第" + (i + 1) + "次加锁执行中\t"+
ClassLayout.parseInstance(lock).toPrintable());
}
}
}
},"thread3").start();
Thread.sleep(3000);
log.debug("查看新创建的对象");
log.debug((ClassLayout.parseInstance(new Object()).toPrintable()));
LockSupport.park();
}
总结
- 批量重偏向和批量撤销是针对类的优化,和对象无关。
- 偏向锁重偏向一次之后不可再次重偏向。
- 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,
剥夺了该类的新实例对象使用偏向锁的权利
锁之间的流程图解
关于偏向锁轻量级锁重量级锁存在的理解误区:
1. 无锁——>偏向锁——>轻量级锁——>重量级2锁 (不存在无锁——>偏向锁)
2.轻量级锁自旋获取锁失败,会膨胀升级为重量级锁 (轻量级锁不存在自旋)
3. 重量级锁不存在自旋 (重量级锁存在自旋 )
自旋优化
重量级锁也会自旋,进行优化,
- 自旋会占用cpu,,多核有优势
- Java6之后,自适应自旋,就是成功过会认为再次成功的可能性较高,多试几次,
反之就会减少次数,或不自旋 - Java7之后就不能关闭这种自适应自旋,
注意:自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)
锁粗化
连续操作会对同一个对象反复加锁解锁,即使没有出现竞争,也会有性能损耗,jvm会对连串的操作同一个对象的锁,扩大加锁返回
StringBuffer buffer = new StringBuffer();
/**
* 锁粗化
*/
public void append(){
buffer.append("aaa").append(" bbb").append(" ccc");
}
buffer.append 方法都需要加锁和解锁.,第一次append方法时进行加锁,最后一次append方法结束后进行解锁
锁消除
即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间
public class LockEliminationTest {
/**
* 锁消除
* -XX:+EliminateLocks 开启锁消除(jdk8默认开启)
* -XX:-EliminateLocks 关闭锁消除
* @param str1
* @param str2
*/
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
public static void main(String[] args) throws InterruptedException {
LockEliminationTest demo = new LockEliminationTest();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
demo.append("aaa", "bbb");
}
long end = System.currentTimeMillis();
System.out.println("执行时间:" + (end - start) + " ms");
}
StringBuffer的append是个同步方法,但是append方法中的 StringBuffer 属于一个局部变量,不可能从该方法中逃逸出去,因此其实这过程是线程安全的,可以将锁消除。
逃逸分析(Escape Analysis)
逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态作用域
方法逃逸(对象逃出当前方法)
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
线程逃逸((对象逃出当前线程)
这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。
使用逃逸分析后会做出如下的优化
- 同步省略或锁消除,如果一个对象被发现,一个对象只能从一个线程访问,那就对于这个对象的操作不考虑同步
- 将堆分配转化为栈分配,
- 分离对象或标量替换:
jdk6才开始引入该技术,jdk7开始默认开启逃逸分析。在Java代码运行时,可以通过JVM参数指定是否开启逃逸分析:
-XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启)
-XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启)
-XX:-DoEscapeAnalysis //表示关闭逃逸分析。
-XX:+EliminateAllocations //开启标量替换(默认打开)
/**
* @author Fox
*
* 进行两种测试
* 关闭逃逸分析,同时调大堆空间,避免堆内GC的发生,如果有GC信息将会被打印出来
* VM运行参数:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 开启逃逸分析 jdk8默认开启
* VM运行参数:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 执行main方法后
* jps 查看进程
* jmap -histo 进程ID
*
*/
@Slf4j
public class EscapeTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
alloc();
}
long end = System.currentTimeMillis();
log.info("执行时间:" + (end - start) + " ms");
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
/**
* JIT编译时会对代码进行逃逸分析
* 并不是所有对象存放在堆区,有的一部分存在线程栈空间
* Ponit没有逃逸
*/
private static String alloc() {
Point point = new Point();
return point.toString();
}
/**
*同步省略(锁消除) JIT编译阶段优化,JIT经过逃逸分析之后发现无线程安全问题,就会做锁消除
*/
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
/**
* 标量替换
*
*/
private static void test2() {
Point point = new Point(1,2);
System.out.println("point.x="+point.getX()+"; point.y="+point.getY());
// int x=1;
// int y=2;
// System.out.println("point.x="+x+"; point.y="+y);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Point{
private int x;
private int y;
通过 jmap -histor pid
查看对象个数以及占用的内存大小