文章目录
- Synchronized
- 概念
- 自增自减字节码指令
- 临界区
- 竞态条件
- 基本使用
- 原理
- 查看synchronized的字节码指令序列
- Monitor
- 对象的内存布局
- Mark Word是如何记录锁状态的
- 偏向锁
- 什么是偏向锁
- 偏向锁延迟偏向
- 偏向锁状态跟踪
- 偏向锁撤销之调用对象HashCode
- 偏向锁撤销之调用wait/notify
- 轻量级锁
- 重量级锁
- 锁升级场景
- synchronized锁优化
- 偏向锁批量重偏向与批量撤销
- 自旋优化
- 锁粗化
- 锁消除
Synchronized
概念
自增自减字节码指令
我们知道自增自减操作不是原子性的,一行代码它为四条指令
getstatic i // 获取静态变量i的值
iconst_1 // 将int常量1压入操作数栈
iadd // 自增 自减指令是isub
putstatic i // 将修改后的值存入静态变量i中
既然不是原子操作,那么就有可能在最后一步取出操作数栈结果之前进行了线程上下文切换,进而导致线程安全问题
临界区
多个线程对共享资源进行读写操作就会有并发安全问题。
临界区:一段代码对共享资源进行读写操作,这段代码称为临界区
临界资源:共享资源称为临界资源
//临界资源
private static int counter = 0;
public static void increment() { //临界区
counter++;
}
public static void decrement() {//临界区
counter--;
}
竞态条件
多个线程对共享资源有竞争,那么也就有竞态条件
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果也无法预测,称为发生了竞态条件
避免临界区中竞态条件发生:
- 阻塞式解决方案:加锁
- 非阻塞式解决方案:CAS原子变量
基本使用
synchronized如果锁对象是类class对象,它是不存在偏向锁的。
private static String lock = "";
public static void increment() {
synchronized (lock){
counter++;
}
}
public static void decrement() {
synchronized (lock) {
counter--;
}
}
接下来的一个执行流程时序图如下所示
原理
在JDK1.5之前,synchronized是基于Monitor机制实现的,其实就是管程。依赖底层操作系统的互斥原语Mutex互斥量,所以就涉及到用户态到内核态的切换。是重量级锁,性能较低。
在JDK1.5之后,添加了锁粗化、锁消除、轻量级锁、偏向锁、自适应自旋等技术减少锁操作的开销。
同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit来实现。
两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
查看synchronized的字节码指令序列
首先是synchronized添加在方法上,通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现
接下来是同步代码块方式,通过monitorenter和monitorexit来实现
Monitor
monitor,翻译是监视器,在操作系统层面叫管程,管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。
在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型
Java语言的内置管程synchronized
java语言内置的管程(synchronized)参考了MESA管程模型,并对它进行了精简。在MESA中条件变量有多个,而java语言内置的管程只有一个条件变量
Monitor机制在Java中的实现
java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于ObjectMonitor
实现,这是 JVM 内部基于 C++ 实现的一套机制。
ObjectMonitor其主要数据结构如下:
ObjectMonitor() {
_header = NULL; //对象头 markOop
_count = 0;
_waiters = 0,
_recursions = 0; // 锁的重入次数
_object = NULL; //存储锁对象
_owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
_WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
FreeNext = NULL ;
_EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失败的线程)
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
ObjectMonitor中有三个阻塞队列:_cxq 、_EntryList、_WaitSet
。刚开始多个线程竞争锁,竞争失败的线程进入到_cxq
队列中,它是栈结构。
获取到锁的对象执行后续的业务逻辑,调用等待方法后进入_WaitSet
队列中,被唤醒后根据相应策略进入_cxq
或_EntryList
队列中。
当持有锁对象的线程释放锁后,会根据相应的策略去唤醒_cxq
或_EntryList
队列中的线程。
默认策略(QMode=0)是:_EntryList
队列中不为空,直接从_EntryList
队列中唤醒线程。如果_EntryList
队列为空,则将_cxq
中的元素插入到_EntryList
,并唤醒第一个线程,也就是后来的线程先获取到锁。
对象的内存布局
一个对象是由三部分组成:对象头、实例数据、对其填充
而对象头由三部分组成:Mark Word标记、元数据指针、数组长度
- Mark Work标记:用于标记对象hash值、分代年龄、锁状态标记、线程持有的锁、偏向线程ID、偏向时间戳等。32位机器占4字节,64为机器占8字节
- Klass point指针:指向方法区中类元数据,标识当前对象是哪个类的实例,开启指针压缩后占4字节
- 数组长度:数组对象才有,占四字节
Mark Word是如何记录锁状态的
synchronized加锁是加在对象上的,锁对象是如何记录锁状态的嘞?
锁的信息都是记录在每个对象 对象头的Mark Word中的
32位JVM下的对象头Mark Word结构描述
64位JVM下的对象头Mark Word结构描述
详情:
-
hashCode:对象的hashCode值
-
age:分代年龄
-
biased_lock:偏向锁标记位。
因为无锁和偏向锁都是使用的01锁标志位,这样没办法区分所以就有加了1位来标识是否是偏向锁
-
lock:锁标志位
区分锁的状态,01表示无锁或偏向锁、00表示轻量级锁、10表示重量级锁、11表示对象待GC回收状态
-
JavaThread*:保存持有偏向锁的线程ID,这个不是java中的线程ID,它们不一样
偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
-
epoch:保存偏向时间戳
偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
-
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针
当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针。
-
ptr_to_heavyweight_monitor:重量级锁状态下,会创建一个Monitor对象,指向对象监视器Monitor的指针。
如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。
Mark Word中锁标记枚举
enum { locked_value = 0, //00 轻量级锁
unlocked_value = 1, //001 无锁
monitor_value = 2, //10 监视器锁,也叫膨胀锁,也叫重量级锁
marked_value = 3, //11 GC标记
biased_lock_pattern = 5 //101 偏向锁
}
我们新写一个类,使用JOL工具查看对象的内存布局可以发现,刚开始创建一个对象时,它是001无锁状态,然后用synchronized把这个对象变为锁对象后,它是00轻量级锁状态了。那为什么会跳过偏向锁直接变为了轻量级锁嘞?
偏向锁
什么是偏向锁
偏向锁是一种加锁操作的优化机制。经过研究发现大部分情况下是不存在锁竞争,一直都是一个线程去获取锁,因此为了消除在无竞争情况下重入锁(CAS操作)的开销,而引入了偏向锁。
对于没有竞争的场合,偏向锁有很好的优化效果。
JVM1.6默认开启偏向锁。新创建一个对象,此时给对象的Mark Word中的ThreadID为0,说明该对象处于可偏向但未偏向任何线程,也叫作匿名偏向状态
偏向锁延迟偏向
偏向锁是延迟开启的,这也是为什么我们直接运行一个java类,使用JOL工具查看对象的内存布局时发现对象的锁状态会直接从无锁变为轻量级锁。
之所以有偏向锁延迟的原因是:JVM在启动过程中会有一系列复杂的过程,比如装载配置、系统类初始化等等。在这个过程中会大量使用synchronized来为对象加锁,而且这些锁大多数都不是一个线程用,如果直接使用偏向锁,那么就会存在偏向锁撤销、偏向锁升级等过程。为了减少初始化时间,JVM才默认延迟开启偏向锁。
HotSpot虚拟机默认在启动后4s延迟才会对每个新创建的对象开启偏向锁模式。
相关的JVM启动参数
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking
//启用偏向锁
-XX:+UseBiasedLocking
4s后新创建的对象就开启了偏向锁标识,此时ThreadID还是为0
当有一个线程使用synchronized给这个对象加锁后,就会记录ThreadID
偏向锁状态跟踪
上面锁对象是在4s后创建的一个对象,那如果锁对象是某个类的class对象嘞?
这其实就是从无锁01 --> 轻量级锁00 因为类是class对象是在4s之内创建的。
// 锁对象是class对象
public static void main(String[] args) throws InterruptedException {
// 未加锁
System.out.println(ClassLayout.parseInstance(ObjectTest.class).toPrintable());
// 加锁后
new Thread(()->{
synchronized (ObjectTest.class){
System.out.println(ClassLayout.parseInstance(ObjectTest.class).toPrintable());
}
},"Thread1").start();
}
偏向锁加完锁,并释放后的状态,都是101偏向锁状态
public static void main(String[] args) throws InterruptedException {
//jvm延迟偏向
Thread.sleep(5000);
// 创建时
Object obj = new Test();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
new Thread(()->{
// 加锁后
synchronized (obj){
System.out.println(Thread.currentThread().getName()+"获取锁\n"+ClassLayout.parseInstance(obj).toPrintable());
}
// 释放后
System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
},"Thread1").start();
}
如果线程1释放偏向锁后,线程2又加锁了,此时偏向锁会升级为轻量级锁,也有可能还是偏向锁
public static void main(String[] args) throws InterruptedException {
//jvm延迟偏向
Thread.sleep(5000);
Object obj = new Test();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 线程1先加偏向锁,再释放锁
new Thread(()->{
synchronized (obj){
System.out.println(Thread.currentThread().getName()+"\n"+ClassLayout.parseInstance(obj).toPrintable());
}
System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
},"Thread1").start();
// 线程1释放锁后再启动线程2
Thread.sleep(2000);
// 线程2再去加锁
new Thread(()->{
synchronized (obj){
System.out.println(Thread.currentThread().getName()+"\n"+ClassLayout.parseInstance(obj).toPrintable());
}
},"Thread2").start();
}
升级为轻量级锁的情况
还是偏向锁的情况
如果线程1还未释放锁,此时出现了锁竞争,一次CAS获取不到锁之后会进入到锁膨胀,锁膨胀过程中会创建Moniter对象,并且还会进行CAS自旋,如果还未获取到锁就park()阻塞。涉及到moniter对象开始就已经是重量级锁的。
new Thread(()->{
synchronized (obj){
// 锁竞争前 偏向锁101
System.out.println(Thread.currentThread().getName()+"获取锁\n"+ClassLayout.parseInstance(obj).toPrintable());
// 在这其中出现另一个线程来竞争锁
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 发生锁竞争后 重量级锁10
System.out.println(Thread.currentThread().getName()+"锁竞争后\n"+ClassLayout.parseInstance(obj).toPrintable());
}
// 释放锁后 重量级锁10。
// 重量级锁释放锁后会变为无锁状态,因为重量级锁需要操作Moniter,还需要用户态内核态切换,有一定耗时,所以这里释放锁后立刻打印10状态
System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
},"Thread1").start();
Thread.sleep(1000);
new Thread(()->{
synchronized (obj){
// 锁竞争 重量级锁10
System.out.println(Thread.currentThread().getName()+"获取锁\n"+ClassLayout.parseInstance(obj).toPrintable());
}
},"Thread2").start();
偏向锁撤销之调用对象HashCode
补充:偏向锁撤销必须要到GC的安全点才会去进行
在对象头的Mark Word标识中我们知道,无锁状态下会保存对象的HashCode值。而轻量级锁状态下保存的是ThreadId和偏向时间戳。
如果我们调用了对象的hashCode()方法之后,偏向锁就会撤销。因为对于一个对象,其HashCode只会生成一次并保存
- 无锁状态下对象的hashCode保存在对象头的Mark Word中
- 轻量级锁状态下的hashCode保存在栈中锁记录中
- 重量级锁状态下的hashCode保存在Monitor对象中
偏向锁调用hashCode()方法 撤销还分为下面两种情况
- 偏向锁在未加锁时调用hashCode()方法后,对象会变为无锁状态01
- 偏向锁在加锁时调用hashCode()方法后,对象会变为重量级锁状态10
如下所示:
// 偏向锁在未加锁时调用hashCode()方法后,对象会变为无锁状态01
public static void main(String[] args) throws InterruptedException {
//jvm延迟偏向
Thread.sleep(5000);
Object obj = new Test();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
obj.hashCode();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
// 偏向锁在已加锁时调用hashCode()方法后,对象会变为重量级锁状态10
public static void main(String[] args) throws InterruptedException {
//jvm延迟偏向
Thread.sleep(5000);
Object obj = new Test();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
new Thread(()->{
synchronized (obj){
// 加锁之后
obj.hashCode();
System.out.println(Thread.currentThread().getName()+"\n"+ClassLayout.parseInstance(obj).toPrintable());
}
},"Thread1").start();
// 偏向锁在未加锁时调用hashCode()方法后,对象会变为无锁状态01
public static void main(String[] args) throws InterruptedException {
//jvm延迟偏向
Thread.sleep(5000);
Object obj = new Test();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
new Thread(()->{
synchronized (obj){
System.out.println(Thread.currentThread().getName()+"\n"+ClassLayout.parseInstance(obj).toPrintable());
}
// 在释放锁之后调用hashCode()
obj.hashCode();
System.out.println(Thread.currentThread().getName()+"释放锁\n"+ClassLayout.parseInstance(obj).toPrintable());
},"Thread1").start();
}
偏向锁撤销之调用wait/notify
因为wait()、notify()、notifyAll()方法只能用在synchronized代码块中,那么就不需要考虑同步代码块外的情况了。
如果在synchronized代码段中调用wait()
方法,那么锁状态从会偏向锁101 变为 重量级锁10
如果在synchronized代码段中调用notify()
方法,那么锁状态从会偏向锁101 变为 轻量级锁00
轻量级锁
偏向锁撤销后,并不会立刻直接加重量级锁,而是会先尝试加轻量级锁。一次CAS失败后就会进入到重量级锁逻辑,轻量级锁中没有自旋,锁膨胀中才有自旋,锁膨胀是重量级锁中的。
轻量级锁加锁流程:
-
轻量级锁是从无锁状态开始加的,首先判断当前锁标记,如果是001无锁状态,那么先拷贝一份Mark Word数据到栈中,接下来进行CAS操作修改对象头中指针指向栈内锁记录。栈中锁记录还有一个obj指针,它指向锁对象,相当于是一个双向指针
如果CAS失败,直接锁膨胀
-
如果不是001无锁状态,会进行轻量级锁重入逻辑判断,判断Mark Word中的
ptr_to_lock_record
指针是不是指向当前线程栈帧,可重入的话再入栈一个锁记录到栈中,ptr_to_lock_record
指针指向新的锁记录 -
不可重入,接下来就是膨胀锁的逻辑了。首先拿到一个ObjectMonitor对象。真正的锁竞争发生在重量级锁的逻辑中。
下面的流程是多线程执行的,创建ObjectMonitor对象的逻辑如下
- 当前锁状态是否已经是10重量级锁了,如果是则直接返回已经创建好的Monitor对象,表示已经有其他线程创建了Monit对象并加了重量级锁了
- 判断一个字段标识,当前是否是其他线程在创建Monitor对象中,如果是则使用yield自旋15次,15次之后如果标识位还是创建过程中就park()方法阻塞
- 如果没有其他线程去创建ObjectMonitor,那么当前线程就可以去进行创建逻辑。创建并初始化Monitor,CAS修改锁对象Mark Word指针,如果能成功则更新锁对象头Mark Word设置为重量级锁状态,如果CAS不能成功就释放Monitor对象重试
只要到了创建Monitor对象这一步其实就进入了重量级锁逻辑,其中只要一个线程能创建Monitor对象成功,并修改锁状态为10,然后其他线程先获取到Monitor对象然后进行锁膨胀中的自旋流程,自旋结束后还没竞争到锁就调用park()进入阻塞,等待被唤醒
之所以会有几次CAS自旋的原因是避免直接调用park()阻塞线程,会有用户态和内核态之间来回切换。
在释放轻量级锁时需要进行Mark Word恢复,把锁记录数据拷贝回Mark Word,再变为无锁状态
在未释放锁时其他线程来竞争轻量级锁,CAS修改ptr_to_lock_record
是不会成功的。所以需要先释放轻量级锁 --> 无锁 -->其他线程加轻量级锁
轻量级锁重入:会再栈中再存一个锁记录,但这个不需要再保存一份无锁状态下的Mark Word信息,直接存一个null。出栈的话就直接改Mark Word中的指针。
重量级锁
基于Monitor实现,实际上也是基于管程的MESA模型实现的。
重量级锁释放后也会变为无锁状态,释放锁后修改Mark Word锁标记有一定的延迟,不会立刻改。
重量级锁中才有自旋,轻量级锁只会CAS一次。
重量级锁的加锁流程:
-
线程先获取到了ObjectMonitor对象后,进行锁膨胀
-
CAS修改ObjectMonitor对象中的
_owner
,改为当前线程,如果能修改成功就表示成功获取到锁,如果失败继续执行下面判断 -
判断
_owner
是否为当前线程,如果是则进行重入锁逻辑,_recursions
+1 -
两次自适应自旋
-
如果还是没有获取到锁就这边入阻塞队列的逻辑了,先进入到
_cxq
队列中,使用头插法,如果CAS修改head指针成功就表示入队成功,如果CAS失败再去尝试一次获取锁,在循环进行 CAS --> 尝试获取锁 --> CAS -
入队成功后,死循环 {再次去尝试获取锁 --> park()阻塞 --> 唤醒后再去尝试获取锁 --> 自适应自旋 --> 插入一个内存屏障保证可见性 }
-
上一步加锁成功后会从
_cxq
或_EntryList
队列中移除对应的Node
锁升级场景
偏向锁升级为轻量级锁
- 偏向锁先调用
hashCode()
方法撤销,变为无锁状态,再加锁 - 偏向锁出现了不激烈的锁竞争
- 偏向锁在同步代码块中调用
notify()
方法
偏向锁升级为重量级锁
- 偏向锁在同步代码块中调用
wait()
方法 - 偏向锁加锁期间出现了激烈锁竞争,自旋CAS几次后还是加锁失败
轻量级锁升级为重量级锁
- 加锁期间出现了锁竞争,CAS自旋后还是加锁失败
- 一次CAS获取锁失败后会升级到重量级锁,其中会先有锁膨胀
- 膨胀期间主要是会创建Moniter对象,膨胀完成之后就会获取到Moniter对象,之后会有几次CAS获取锁,还有两次自适应自旋,如果还是没有获取到锁就去进行park()阻塞,接下来再等待被唤醒
- 被唤醒 获取到锁后就会加锁,加锁的同时会去修改对象头Mark Word锁标记。
synchronized锁优化
偏向锁批量重偏向与批量撤销
偏向锁问题:一个线程反复进入同步代码块这种场景下,偏向锁带来的性能开销可以忽略不计。但如果其他线程来竞争偏向锁,那么就会出现偏向锁撤销为无锁状态或升级为轻量级,消耗的性能比无锁到轻量级锁还要多。所以如果大量出现偏向锁撤销那么就会对性能有影响。
为了解决上面的问题就有了偏向锁重偏向和批量撤销
原理
以类的class为单位,为每个class维护一个偏向锁撤销的计数器,每当该类的实例对象进行了一次偏向锁撤销 计数器就+1。当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。但经过测试在第19个偏向锁对象开始就进行重偏向了。
每个class对象的对象头中有一个epoch字段,该类新创建的实例对象 Mark Word中epoch初始值为创建该对象时class中的epoch值。
当偏向锁撤销计数器达到阈值20后,假如有有了新的线程来加偏向锁,class的计数器继续增长,当达到批量撤销阀值默认40,JVM就会标记该class为不可偏向,之后创建该class类的实例对象初始锁标记是001无锁状态
应用场景
批量重偏向机制(bulk rebias)是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。
批量撤销机制(bulk revoke)是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。
查看JVM参数
我们可以通过-XX:BiasedLockingBulkRebiasThreshold
和 -XX:BiasedLockingBulkRevokeThreshold
来手动设置阈值
批量重偏向案例:
首先线程1创建50个偏向锁对象,并且都和线程1进行绑定。再加一个sleep保证线程1一直存活,避免JVM线程复用内核线程
线程2在对前40个偏向锁对象进行加锁操作,根据打印的结果就能发现1~18都进行偏向锁撤销,变为了轻量级锁。第19个偏向锁对象进行了重偏向,线程2加的还是偏向锁,只是ThreadID指向了线程2
@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());
}
}
}, "thead2").start();
}
}
关键输出结果如下
# 刚开始创建的对象是偏向锁,并且和线程1绑定了
11:34:02.413 [main] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - 打印thead1,list中第20个对象的对象头:
11:34:03.339 [main] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 18 09 63 (00000101 00011000 00001001 01100011) (1661540357)
4 4 (object header) 89 01 00 00 (10001001 00000001 00000000 00000000) (393)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
# 1~18偏向锁进行了偏向锁撤销,再次加锁就变为了轻量级锁
11:34:03.341 [thead2] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread2-第16次加锁执行中 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 00 f2 bf 6b (00000000 11110010 10111111 01101011) (1807741440)
4 4 (object header) 4c 00 00 00 (01001100 00000000 00000000 00000000) (76)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11:34:03.341 [thead2] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread2-第17次加锁执行中 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 00 f2 bf 6b (00000000 11110010 10111111 01101011) (1807741440)
4 4 (object header) 4c 00 00 00 (01001100 00000000 00000000 00000000) (76)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11:34:03.342 [thead2] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread2-第18次加锁执行中 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 00 f2 bf 6b (00000000 11110010 10111111 01101011) (1807741440)
4 4 (object header) 4c 00 00 00 (01001100 00000000 00000000 00000000) (76)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11:34:03.342 [thead2] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread2-第18次释放锁 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 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
# 第19个偏向锁开始 还是偏向锁 只是进行了偏向锁重偏向
11:34:03.343 [thead2] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread2-第19次加锁执行中 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e9 48 63 (00000101 11101001 01001000 01100011) (1665722629)
4 4 (object header) 89 01 00 00 (10001001 00000001 00000000 00000000) (393)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11:34:03.343 [thead2] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread2-第20次加锁执行中 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e9 48 63 (00000101 11101001 01001000 01100011) (1665722629)
4 4 (object header) 89 01 00 00 (10001001 00000001 00000000 00000000) (393)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11:34:03.344 [thead2] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread2-第20次释放锁 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e9 48 63 (00000101 11101001 01001000 01100011) (1665722629)
4 4 (object header) 89 01 00 00 (10001001 00000001 00000000 00000000) (393)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
批量重撤销案例:
List集合中有50个偏向锁对象,都和线程1绑定了,线程1加一个sleep保证线程存活
线程2来加锁,其中1~18就都会变为轻量级锁,之后的还是偏向锁,线程2加一个sleep保证线程存活
接下来再来了线程3来加锁,将class偏向锁计数器达到40,然后再新建一个对象就会发现初始初始锁标记是001
注意:时间-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);
// 线程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 <= 49) {
log.debug("thread3-第" + (i + 1) + "次准备加锁\t" +
ClassLayout.parseInstance(lock).toPrintable());
}
synchronized (lock) {
if (i >= 17 && i <= 21 || i >= 35 && i <= 49) {
log.debug("thread3-第" + (i + 1) + "次加锁执行中\t" +
ClassLayout.parseInstance(lock).toPrintable());
}
}
}
}, "thread3").start();
Thread.sleep(3000);
log.debug("查看新创建的对象");
log.debug((ClassLayout.parseInstance(new Object()).toPrintable()));
}
}
关键输出结果
# 线程3开始加锁 1~18锁对象是从无锁到轻量级锁
11:46:45.627 [thread3] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread3-第18次准备加锁 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 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11:46:45.627 [thread3] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread3-第18次加锁执行中 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c0 f4 4f 6f (11000000 11110100 01001111 01101111) (1867510976)
4 4 (object header) f8 00 00 00 (11111000 00000000 00000000 00000000) (248)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
# 之后的锁是从偏向锁到轻量级锁
11:46:45.628 [thread3] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread3-第19次准备加锁 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b1 a0 b3 (00000101 10110001 10100000 10110011) (-1281314555)
4 4 (object header) ff 01 00 00 (11111111 00000001 00000000 00000000) (511)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11:46:45.629 [thread3] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread3-第19次加锁执行中 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c0 f4 4f 6f (11000000 11110100 01001111 01101111) (1867510976)
4 4 (object header) f8 00 00 00 (11111000 00000000 00000000 00000000) (248)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11:46:45.629 [thread3] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread3-第20次准备加锁 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b1 a0 b3 (00000101 10110001 10100000 10110011) (-1281314555)
4 4 (object header) ff 01 00 00 (11111111 00000001 00000000 00000000) (511)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
11:46:45.629 [thread3] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread3-第20次加锁执行中 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c0 f4 4f 6f (11000000 11110100 01001111 01101111) (1867510976)
4 4 (object header) f8 00 00 00 (11111000 00000000 00000000 00000000) (248)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
......
13:14:20.906 [thread3] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread3-第50次准备加锁 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e8 24 78 (00000101 11101000 00100100 01111000) (2015684613)
4 4 (object header) de 01 00 00 (11011110 00000001 00000000 00000000) (478)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
13:14:20.906 [thread3] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - thread3-第50次加锁执行中 java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 10 f1 1f 70 (00010000 11110001 00011111 01110000) (1881141520)
4 4 (object header) 0b 00 00 00 (00001011 00000000 00000000 00000000) (11)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
# 循环结束后,新创建的对象是001无锁状态
13:14:23.898 [main] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - 查看新创建的对象
13:14:23.898 [main] DEBUG cn.tulingxueyuan.sync.BiasedLockingTest - 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 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
总结
- 批量重偏向和批量撤销是针对类的优化,和对象无关。
- 偏向锁重偏向一次之后不可再次重偏向。
- 某个类批量撤销之后,JVM就剥夺了该类新实例对象使用偏向锁的权限。
自旋优化
重量级锁在锁竞争时,会通过CAS自旋来优化,目的是减少线程直接阻塞的次数,因为阻塞挂起线程涉及到操作系统调用,存在内核态用户态的切换。
JDK1.6之后自旋是自适应的,JVM去控制自旋次数,比较智能。JDK1.6的时候还可以通过JVM参数来设置是否开启自旋锁、设置自旋次数。
在JDK1.7之后不能控制是否开启自旋功能,自旋锁的参数被取消,自旋次数也由JVM自行控制
锁粗化
一系列连续的操作对同一个对象反复加锁与解锁,甚至加锁操作出现在循环中,即使没有锁竞争但频繁的进行加锁解锁操作也会导致不需要的性能损耗。
JVM就提出来锁粗化的机制。
JVM如果检测到有一连串零碎的操作都是对同一个对象的加锁,那么将会扩大加锁的同步范围到整个操作序列的外部。如下所示
StringBuffer buffer = new StringBuffer();
/**
* 锁粗化
*/
public void append(){
buffer.append("aaa").append(" bbb").append(" ccc");
}
append()
方法是一个同步方法,那么我们执行上面的代码就会频繁的对stringBuffer对象进行加锁与解锁操作。而JVM测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
锁消除
锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
-XX:+EliminateLocks
开启锁消除(jdk8默认开启)
就比如下面这段代码,10个线程有10个锁对象,根本就是一个无用的锁操作。
可以进行测试运行下面的代码首先查看开启锁消除时的耗时,在看关闭锁消除后的耗时
// 线程逃逸:锁对象没有逃逸出当前线程
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Object obj = new Object();
synchronized(obj){
// TODO
}
}).start();
}
// 方法内的局部变量加锁,锁对象没有逃逸出方法
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
锁消除就涉及到了逃逸分析的概念了。JVM有两种逃逸分析:
方法逃逸(对象逃出当前方法):当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
线程逃逸((对象逃出当前线程): 这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。
使用逃逸分析,编译器可以对代码做如下优化:
-
同步省略或锁消除(Synchronization Elimination)。
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
-
将堆分配转化为栈分配(Stack Allocation)。
如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
-
分离对象或标量替换(Scalar Replacement)。
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。