1.Synchronized优化原理
1.1.轻量级锁(Lock Record)
1.1.1.简介
1>.轻量级锁的使用场景:如果一个对象虽然有多个线程访问,但是多个线程访问的时间是错开的(即没有竞争),那么可以使用轻量级锁来进行优化;
2>.轻量级锁对使用者是透明的,即语法仍然是"synchronized"
;
1.1.2.案例
1>.有两个方法同步块,利用同一个对象加锁.代码如下:
public class TestSynchronized {
static final Object obj = new Object();
public static void method1(){
synchronized (obj){
//同步代码块
method2();
}
}
public static void method2(){
synchronized (obj){
//同步代码块
}
}
}
2>.分析:
①.线程执行到第一个synchronized(obj)时,会在栈帧中创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定的对象的Mark Word信息;
②.让锁记录中Object Reference指向锁对象Object,并尝试用CAS替换Object的Mark Word,将Mark Word的值存入锁记录;
使用CAS将锁记录中的"lock record地址和状态00"与Object对象头中的"Mark Word"进行交换;
③.如果CAS替换成功,那么Object对象头中就存储了"锁记录地址和状态00",表示由该线程给对象加锁;
状态'00'表示轻量级锁;
④.如果CAS替换失败,有两种情况:
- 如果是其他线程已经持有了该Object的轻量级锁(即对象头中的Mark Word中的锁状态标记不是’01’或者对象头中的Mark Word中的锁状态标记已经变成’00’,而且锁记录地址指向的是其他线程),这时表明有竞争,进入锁膨胀过程;
- 如果是线程自己执行了synchronized锁重入(即在同步代码块中调用了另一个同步代码块/另一个包含同步代码块的方法),那么在一个新的栈帧中再添加一条锁记录Lock Record作为重入的计数,然后重复②,③步骤,但是本次加锁会失败,可是本次失败并不会对代码的执行产生影响,最终第二次创建的锁记录中的锁记录地址的值是空对象null,表示本次使用锁重入机制;
⑤.(当第二个synchronized(obj)同步代码块执行完毕)当退出synchronized代码块(解锁)时,如果发现有锁记录地址的值为null的锁记录,表示有重入,这时重置锁记录(将该锁记录删除),表示重入计数减1;
⑥.(当第一个synchronized(obj)同步代码执行完毕)当退出synchronized代码块(解锁)时,锁记录中锁记录地址的值不为null,这时会使用CAS将锁记录中之前替换过来的Mark Word的值恢复/还原给Object对象头;
- 如果恢复成功,则解锁成功;
- 如果恢复失败,说明轻量级锁进行了锁膨胀或者已经升级为重量级锁,再进入重量级锁的解锁流程;
⑦.最后清除锁记录;
1.2.锁膨胀(Monitor)
1.2.1.简介
1>.如果线程在尝试给对象加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁,产生了竞争,这时需要进行锁膨胀,将轻量级锁变成重量级锁;
1.2.2.案例
1>.代码如下:
public class TestSynchronized {
static Object obj = new Object();
public static void method1(){
synchronized (obj){
//同步代码块
}
}
}
2>.分析:
①.当线程Thread-1进行轻量级加锁时,线程Thread-0已经对该对象加上了轻量级锁;
此时线程Thread-1加轻量级锁失败,进入锁膨胀流程:
- 为Object对象申请Monitor锁(重量级锁),让Object对象指向重量级锁的地址(Object对象头中的Mark Word变成/设置Monitor对象的地址,并且锁状态标识从’00’变成’10’),
- 之后线程Thread-1自己进入Monitor对象的EntryList等待队列中进行BLOCKED阻塞,等待被唤醒;
- 当线程Thread-0执行完同步代码块解锁时,使用CAS将锁记录中之前替换过来的Mark Word的值恢复给Object对象的对象头,此时必然恢复失败,这时就会进入重量级锁的解锁流程,即按照Object对象头中Monitor地址找到Monitor对象,设置Monitor对象的Owner属性值为null,然后唤醒EntryList等待队列中处于BLOCKED阻塞状态的线程竞争锁,而竞争的过程是非公平的(并不一定是先到先得);
1.3.自旋优化
1.3.1.简介
1>.多个线程在重量级锁(Monitor)竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即循环重试获取Monitor锁的过程中持锁线程退出了同步块,释放了锁),那么当前线程就可以避免阻塞,直接成为Monitor锁的所有者;
某个线程获取锁的时候,发现Monitor对象的Owner已经指向了其他的线程,即Monitor锁被其他线程占用了,那么这个线程先不进入EntryList等待队列中进行BLOCKED阻塞,而是循环重试几次获取Monitor锁,如果在循环重试获取锁的过程中,其他持锁线程执行完同步代码块中的代码,释放了Monitor锁,那么当前线程就可以避免阻塞,直接成为Monitor锁的所有者;如果循环重试几次之后,其他持锁线程一直没有执行完同步代码块的代码,一直没有释放锁,那么当前线程最终还是会进入EntryList等待队列中进行BLOCKED阻塞;
2>.自旋重试成功的情况:
注意:由上图可知,自旋优化只有在多核CPU环境下才能发挥作用,在单核CPU环境下没有效果!
3>.自旋重试失败的情况:
注意:
①.在Java6之后自旋锁是自适应的,比如线程刚刚的一次自旋操作成功过,那么就可以认为本次自旋成功的可能性会高,那么本次就会多自旋几次;反之,就会减少自旋次数甚至不自旋,总之,就是比较智能;
②.自旋会占用CPU时间片,单核CPU环境下自旋就是浪费,没有意义,多核CPU环境下自旋才能发挥优势;
③.Java7之后不能控制是否开启自旋功能(由jvm底层控制);
1.4.偏向锁
1.4.1.简介
1>.轻量级锁在没有竞争的时候(就自己这个线程),每次重入仍然需要执行CAS替换操作;
2>.Java6中引入了偏向锁来做进一步优化: 只要第一次加锁时成功使用CAS操作将线程ID设置到对象的Mark Word头中,那么之后再次加锁时发现对象头的Mark Word中这个线程ID是自己的就表示没有竞争,不用重新CAS操作.以后只要不发生竞争,这个锁对象就归该线程所有(即对象头的MarkWord中会一直存放该线程ID);
1.4.2.案例
1>.代码如下:
public class TestSynchronized2 {
static final Object obj = new Object();
public static void m1(){
synchronized (obj){
//同步块A
m2();
}
}
public static void m2(){
synchronized (obj){
//同步块B
m3();
}
}
public static void m3(){
synchronized (obj){
//同步块C
}
}
}
2>.分析:
1.4.3.偏向状态
回忆一下对象头格式:
第一行是无锁状态;
第二行是偏向锁;
第三行是轻量级锁;
第四行是重量级锁;
倒数第三位的"biased_lock"是偏向锁的状态,'0’表示没有启用偏向锁,'1’表示启用了偏向锁;
1>.说明: 一个对象创建时
①.如果开启了偏向锁(默认开启),那么对象创建之后,MarkWord值为’0x05’,即最后三位为’101’(对象处于一个可以偏向的状态),这时它的thread,epoch,age都是’0’;
②.偏向锁默认是延迟的,不会在程序启动时立即生效,如果想要避免延迟,可以通过JVM参数’-XX:BiasedLockingStartupDelay=0’来禁用延迟;
③.如果没有开启偏向锁,那么对象创建之后,MarkWord值为’0x01’,即最后三位为’001’,这时它的hashcode,age都是’0’,第一次用到hashcode时才会赋值;
④.处于偏向锁的对象解锁之后,线程ID仍然存储在对象头中;
2>.代码示例:
@Slf4j
public class TestSynchronized3 {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
//使用jol工具类打印对象信息
log.info(ClassLayout.parseInstance(dog).toPrintable());
}
}
class Dog{
}
注意:查看对象的对象头信息需要借助一个工具'jol',在maven项目中添加其依赖即可!
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
3>.禁用偏向锁
①.修改程序的jvm启动参数为"-XX:-UseBiasedLocking";
②.在启用偏向锁的状态下,在加锁之前调用"对象实例.hashcode()"也会禁用偏向锁;
- 因为调用了’对象实例.hashcode()‘会将对象的hashcode填充到对象头的MarkWord中,由于此时MarkWord中存储了偏向锁要使用的线程ID(54位),导致对象的hashcode没有存储空间(位置)了,所以需要撤销对象的偏向状态(MarkWord值后三位从’101’变成’001’),清除偏向锁对应的信息,对象恢复到原始状态;
③.在启用了轻量级锁的状态下,调用’对象实例.hashcode()'并不会禁用轻量级锁,因为使用轻量级锁时对象的hashcode存放在线程栈帧的锁记录中;
④.在启用了重量级级锁的状态下,调用’对象实例.hashcode()'并不会禁用重量级锁,因为使用重量级锁时对象的hashcode存放在monitor对象中;
1.4.4.偏向锁撤销
1>.当有其它线程使用偏向锁对象(/处于偏向状态的对象)时,会将偏向锁升级为轻量级锁;
2>.示例代码:
@Slf4j
public class TestSynchronized3 {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
new Thread(() -> {
//加锁前打印对象信息
log.info(ClassLayout.parseInstance(dog).toPrintable());
//加锁后打印对象信息
synchronized (dog) {
log.info(ClassLayout.parseInstance(dog).toPrintable());
}
//解锁后打印对象信息
log.info(ClassLayout.parseInstance(dog).toPrintable());
synchronized (TestSynchronized3.class){
//t2线程运行的时需要等待t1线程运行完毕才能继续运行,保证两个线程是错开运行的
//t1线程执行完毕之后,唤醒正在等待中的线程;
TestSynchronized3.class.notify();
}
}, "t1").start();
new Thread(() -> {
//t2线程运行的时需要等待t1线程运行完毕才能继续运行,保证两个线程是错开运行的
synchronized (TestSynchronized3.class){
try {
TestSynchronized3.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("线程t2运行....");
//加锁前打印对象信息
log.info(ClassLayout.parseInstance(dog).toPrintable());
//加锁后打印对象信息
synchronized (dog) {
log.info(ClassLayout.parseInstance(dog).toPrintable());
}
//解锁后打印对象信息
log.info(ClassLayout.parseInstance(dog).toPrintable());
}, "t2").start();
}
}
class Dog {
}
结论:
当有其它线程使用偏向锁对象(/处于偏向状态的对象)时,会将偏向锁升级为轻量级锁!
注意: 调用wait/notify也可以撤销偏向锁;
1.4.5.批量重偏向
1>.如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏向T2,重偏向会重置对象的Thread ID;
2>.当撤销偏向锁阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢(偏向T1)?于是会在给这些对象加锁时重新偏向至加锁线程(偏向T2);
当撤销偏向锁阈值超过20次之后,对象就会一直使用偏向锁;
3>.代码示例
@Log4j2
public class TestSynchronized4 {
public static void main(String[] args) throws InterruptedException {
Vector<Dog> list = new Vector<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}
synchronized (list) {
list.notify();
}
}, "t1");
t1.start();
Thread t2 = new Thread(() -> {
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("===============> ");
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.info(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}, "t2");
t2.start();
}
}
class Dog{}
1.4.6.批量撤销
1>.当撤销偏向锁阈值超过40次后
,JVM会这样觉得,自己确实偏向错了,根本就不该偏向.于是整个类的所有对象都会变为不可偏向的(对象恢复到最原始状态),新建的对象也是不可偏向的
;
1.4.7.锁消除
public class MyBenchmark {
static int x = 0;
public void a() throws Exception {
x++;
}
//Java程序在运行的时候,JVM底层会通过一个JIT(即时编译器)对Java字节码做一步优化,
//其中一个优化手段就是分析局部变量(锁对象)是否可以优化,如果局部变量(锁对象)的作用范围
//不会发生逃逸/暴露,这就意味着该局部变量(锁对象)不可能被多个线程共享,此时对该局部变量(锁
//对象)加锁是没有任何意义的,也就是说这里的synchronized并不会消耗系统性能.
//这就是锁消除优化;
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
}
关闭锁消除优化的JVM参数: -XX:-EliminateLocks
1.4.8.锁粗化
1>.对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化;
2>.示例代码:
//频繁的进行加锁、解锁,会造成性能上的损失.如果虚拟机探测到有一串零碎操作都是对同一对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部
for(int i=0;i<100000;i++){
synchronized(this){
}
}
//在锁粗化之后运行逻辑如下列代码
//锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作
synchronized(this){
for(int i=0;i<100000;i++){
}
}