synchronized优化

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
  1. 如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间;
  1. 如果对于那些需要同步的简单的代码块,获取锁挂起操作消耗的时间比用户代码执行的时间还要长,这种同步策略显然非常糟糕的。
synchronized会导致争用不到锁的线程进入阻塞状态(涉及到系统调用),所以说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。
static final Object lock = new Object(); static int counter = 0; public static void main(String[] args) { synchronized (lock) { counter++; } }
对应字节码为
notion image
synchronized的字节码指令会确保能够进行解锁,避免死锁情况出现

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法是synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object(); public static void method1() { synchronized (obj) { //同步块A method2(); } } public static void method2(){ synchronized (obj){ //同步块B } }
  • 创建锁记录(Lock Record)对象,每个线程中的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的MarkWord
    • notion image
       
  • 让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将MarkWord的值存入锁记录(如果markWord中有其他栈帧的锁记录,则这步失败)
    • notion image
       
  • 如果CAS替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下
    • notion image
       
  • 如果CAS失败,有两种情况
    • 如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
    • 如果是自己执行了synchronized锁重入,那么再向对象头添加一条为null的Lock Record作为重入的计数
notion image
 
  • 当退出synchronized代码块(解锁时)如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
    • notion image
  • 当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用cas将MarkWord的值恢复给对象头
    • 成功,则解锁成功. 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,引入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
static final Object obj = new Object(); public static void method1() { synchronized (obj) { //同步块 } }
当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁
notion image
 
这时Thread-1加轻量级锁失败,进入锁膨胀过程
  • 为Object对象申请Monitor锁,让Object指向重量级锁地址
  • 然后自己进入Monitor的EntryList BLOKCED
  • 当Thread-0退出同步块解锁时,使用CAS将Mark word的值恢复给对象头,失败,这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner 为null,唤醒EntryList中Blocked线程
notion image
 

自旋优化

Java的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态装换需要耗费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的getter()和setter()方法),状态转换消耗的时间有可能比用户代码执行的时间还要长。
虚拟机的开发团队注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间取挂起和恢复现场并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下“,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
自旋等待不能代替阻塞。自
旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源。
因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当使用传统的方式去挂起线程了。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续更长的时间,比如100次循环。如果某个锁,自旋很少成功获得过,那在以后要获得这个锁时将可能省略掉自旋过程,以免浪费处理器资源。
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞
自旋重试成功的情况
notion image
 
自旋重试失败的情况
notion image
 
  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

偏向锁

轻量级锁在没有竞争时(就自己这个线程);每次重入任然需要执行CAS操作,java6中引入了偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有
例如
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 } }
notion image
 
notion image

偏向状态

对象头格式
notion image
一个对象创建时:
  • 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
  • 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 - XX:BiasedLockingStartupDelay=0 来禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

1、测试延迟特性

@Slf4j(topic = "c.TestBiased") public class TestBiased { public static void main(String[] args) throws IOException, InterruptedException { test1(); } // 测试偏向锁 private static void test1() { Dog d = new Dog(); log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(SimpleClassLayout.parseInstance(new Dog()).toPrintSimple(true)); } } class Dog { }
直接执行main方法,结果如下
20:27:26.304 c.TestBiased [main] - 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 锁标识位为: 001 锁状态为: 无锁 20:27:30.313 c.TestBiased [main] - 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 锁标识位为: 101 锁状态为: 偏向锁
可以看出一开始偏向锁并没有启用,4秒延迟过后才会启用偏向锁
  • JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。这个延时的时间大概为4s左右,具体时间因机器而异
 
main方法加上jvm参数XX:BiasedLockingStartupDelay=0 再次执行
64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 锁标识位为: 101 锁状态为: 偏向锁 21:30:52.151 c.TestBiased [main] - 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 锁标识位为: 101 锁状态为: 偏向锁
偏向锁立即启用
 

2、 测试偏向锁

@Slf4j(topic = "c.TestBiased") public class TestBiased { // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 public static void main(String[] args) throws IOException, InterruptedException { test1(); } // 测试偏向锁 private static void test1() { Dog d = new Dog(); new Thread(() -> { log.debug("synchronized 前"); log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); synchronized (d) { log.debug("synchronized 中"); log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } log.debug("synchronized 后"); log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); }, "t1").start(); } } class Dog { }
运行main方法结果如下
21:38:04.943 c.TestBiased [t1] - synchronized 前 21:38:07.266 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 锁标识位为: 101 锁状态为: 偏向锁 21:38:07.266 c.TestBiased [t1] - synchronized 中 21:38:07.266 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000010 00011101 11001101 10000111 11001000 00000101 锁标识位为: 101 锁状态为: 偏向锁 21:38:07.266 c.TestBiased [t1] - synchronized 后 21:38:07.266 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000010 00011101 11001101 10000111 11001000 00000101 锁标识位为: 101 锁状态为: 偏向锁
处于偏向锁的对象解锁后,线程 id 仍存储于对象头中
 
 

3、测试禁用

在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁
输出
21:41:45.089 c.TestBiased [t1] - synchronized 前 21:41:48.146 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 锁标识位为: 001 锁状态为: 无锁 21:41:48.146 c.TestBiased [t1] - synchronized 中 21:41:48.146 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000000 10000111 10000111 10111111 11110101 01110000 锁标识位为: 000 锁状态为: 轻量级锁、自旋锁 21:41:48.161 c.TestBiased [t1] - synchronized 后 21:41:48.161 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 锁标识位为: 001 锁状态为: 无锁
 

4、测试 hashCode

正常状态对象一开始是没有 hashCode 的,第一次调用才生成,调用hashCode后,对象会禁用偏向锁,结果和3一致
@Slf4j(topic = "c.TestBiased") public class TestBiased { // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 public static void main(String[] args) throws IOException, InterruptedException { test1(); } // 测试 hashCode private static void test1() { Dog d = new Dog(); log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } d.hashCode(); log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } } class Dog { }
输出结果
22:23:16.786 c.TestBiased [main] - 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 锁标识位为: 101 锁状态为: 偏向锁 22:23:20.790 c.TestBiased [main] - 64位Mark Word信息: 00000000 00000000 00000000 01111100 00001110 00101010 10111101 00000001 锁标识位为: 001 锁状态为: 无锁
 

偏向锁撤销

撤销-调用对象hashCode

 
调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程id,如果调用hashCode会导致偏向锁被取消
  • 轻量级锁会在锁记录中记录hashCode
  • 重量级锁会在Monitor中记录hashCode
@Slf4j(topic = "c.TestBiased") public class TestBiased { // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 public static void main(String[] args) throws IOException, InterruptedException { test1(); } // 测试 hashCode Dog d = new Dog(); new Thread(() -> { log.debug("synchronized 前"); log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); synchronized (d) { log.debug("synchronized 中"); log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } log.debug("synchronized 后"); d.hashCode(); log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); }, "t1").start(); } class Dog { }
 
 

撤销 - 其它线程使用对象

@Slf4j(topic = "c.TestBiased") public class TestBiased { // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 public static void main(String[] args) throws IOException, InterruptedException { test2(); } private static void test2() throws InterruptedException { Dog d = new Dog(); Thread t1 = new Thread(() -> { synchronized (d) { log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } synchronized (TestBiased.class) { TestBiased.class.notify(); } }, "t1"); t1.start(); Thread t2 = new Thread(() -> { synchronized (TestBiased.class) { try { TestBiased.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); synchronized (d) { log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); }, "t2"); t2.start(); } class Dog { }
输出
22:53:44.235 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000010 00111000 01011111 11011111 11101000 00000101 锁标识位为: 101 锁状态为: 偏向锁 22:53:44.242 c.TestBiased [t2] - 64位Mark Word信息: 00000000 00000000 00000010 00111000 01011111 11011111 11101000 00000101 锁标识位为: 101 锁状态为: 偏向锁 22:53:44.242 c.TestBiased [t2] - 64位Mark Word信息: 00000000 00000000 00000000 11110100 10000111 01011111 11101111 11110000 锁标识位为: 000 锁状态为: 轻量级锁、自旋锁 22:53:44.242 c.TestBiased [t2] - 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 锁标识位为: 001 锁状态为: 无锁
2线程等待,t1线程首先获得锁对象d
t2线程获得锁对象,检查对象的markword,发现已经有其他线程的id,升级为轻量级锁
t2线程释放锁对象,锁对象变为无锁状态,不再偏向任何一个线程
 

撤销 - 调用wait,notify

wait,notify底层通过monitor对象实现,所以会从偏向锁升级到重量级锁
@Slf4j(topic = "c.TestBiased") public class TestBiased { public static void main(String[] args) throws IOException,InterruptedException{ test1(); } // 测试偏向锁 private static void test1() { Dog d = new Dog(); Thread t1 = new Thread(() -> { log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); synchronized (d) { log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); try { d.wait(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } }, "t1"); t1.start(); Thread t2 = new Thread(() -> { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (d){ log.debug("notify"); d.notify(); } }, "t2"); t2.start(); } }
输出
20:24:19.482 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 锁标识位为: 101 锁状态为: 偏向锁 20:24:19.482 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000001 11011101 11001000 11111101 11110000 00000101 锁标识位为: 101 锁状态为: 偏向锁 20:24:22.561 c.TestBiased [t2] - notify 20:24:22.561 c.TestBiased [t1] - 64位Mark Word信息: 00000000 00000000 00000001 11011101 11000101 10101010 11110001 01001010 锁标识位为: 010 锁状态为: 重量级锁
 

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID 当撤销偏向锁阈值超过 20 次后(从第20次开始),jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程,而不是升级为轻量级锁
@Slf4j(topic = "c.TestBiased") public class TestBiased { public static void main(String[] args) throws IOException,InterruptedException{ test3(); } private static void test3() 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.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } } synchronized (list) { list.notify(); } }, "t1"); t1.start(); Thread t2 = new Thread(() -> { synchronized (list) { try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("===============> "); for (int i = 0; i < 30; i++) { Dog d = list.get(i); log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintableSimple(true)); } }, "t2"); t2.start(); } }
输出
20:56:58.527 c.TestBiased [t2] - 18 64位Mark Word信息: 00000000 00000000 00000001 01011111 00111011 00010100 01111000 00000101 锁标识位为: 101 锁状态为: 偏向锁 20:56:58.527 c.TestBiased [t2] - 18 64位Mark Word信息: 00000000 00000000 00000000 00010001 11011011 10001111 11110011 01010000 锁标识位为: 000 锁状态为: 轻量级锁、自旋锁 20:56:58.527 c.TestBiased [t2] - 18 64位Mark Word信息: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 锁标识位为: 001 锁状态为: 无锁 20:56:58.527 c.TestBiased [t2] - 19 64位Mark Word信息: 00000000 00000000 00000001 01011111 00111011 00010100 01111000 00000101 锁标识位为: 101 锁状态为: 偏向锁 20:56:58.527 c.TestBiased [t2] - 19 64位Mark Word信息: 00000000 00000000 00000001 01011111 00111011 00010011 11110001 00000101 锁标识位为: 101 锁状态为: 偏向锁 20:56:58.527 c.TestBiased [t2] - 19 64位Mark Word信息: 00000000 00000000 00000001 01011111 00111011 00010011 11110001 00000101 锁标识位为: 101 锁状态为: 偏向锁 //开始偏向线程t2 20:56:58.527 c.TestBiased [t2] - 20 64位Mark Word信息: 00000000 00000000 00000001 01011111 00111011 00010100 01111000 00000101 锁标识位为: 101 锁状态为: 偏向锁 20:56:58.527 c.TestBiased [t2] - 20 64位Mark Word信息: 00000000 00000000 00000001 01011111 00111011 00010011 11110001 00000101 锁标识位为: 101 锁状态为: 偏向锁 20:56:58.527 c.TestBiased [t2] - 20 64位Mark Word信息: 00000000 00000000 00000001 01011111 00111011 00010011 11110001 00000101 锁标识位为: 101 锁状态为: 偏向锁

批量撤销

当撤销偏向锁阈值超过 40 次(从第40次开始)后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
public class TestBiased { public static void main(String[] args) throws IOException, InterruptedException { test4(); } static Thread t1, t2, t3; private static void test4() throws InterruptedException { Vector<Dog> list = new Vector<>(); int loopNumber = 39; t1 = new Thread(() -> { for (int i = 0; i < loopNumber; i++) { Dog d = new Dog(); list.add(d); synchronized (d) { log.debug(i + "\t" + SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } } LockSupport.unpark(t2); }, "t1"); t1.start(); t2 = new Thread(() -> { LockSupport.park(); log.debug("===============> "); for (int i = 0; i < loopNumber; i++) { Dog d = list.get(i); log.debug(i + "\t" + SimpleClassLayout.parseInstance(d).toPrintSimple(true)); synchronized (d) { log.debug(i + "\t" + SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } log.debug(i + "\t" + SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } LockSupport.unpark(t3); }, "t2"); t2.start(); t3 = new Thread(() -> { LockSupport.park(); log.debug("===============> "); for (int i = 0; i < loopNumber; i++) { Dog d = list.get(i); log.debug(i + "\t" + SimpleClassLayout.parseInstance(d).toPrintSimple(true)); synchronized (d) { log.debug(i + "\t" + SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } log.debug(i + "\t" + SimpleClassLayout.parseInstance(d).toPrintSimple(true)); } }, "t3"); t3.start(); t3.join(); log.debug(SimpleClassLayout.parseInstance(new Dog()).toPrintSimple(true)); } }
相关博客

逃逸分析

 
关于 Java 逃逸分析的定义:
逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。
逃逸分析的 JVM 参数如下:
  • 开启逃逸分析:-XX:+DoEscapeAnalysis
  • 关闭逃逸分析:-XX:-DoEscapeAnalysis
  • 显示分析结果:-XX:+PrintEscapeAnalysis
逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数。
逃逸分为两种:
  1. 方法逃逸:当一个对象在方法中被定义后,可能作为调用参数被外部方法所引用。
  1. 线程逃逸:通过复制给类变量或者作为实例变量在其他线程中可以被访问到。
逃逸分析相关优化
如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行一下三种优化:
 
  1. 栈上分配stack allocation:如果对象不会逃逸到方法外,则对此对象在栈上分配内存,则对象所占用的空间可以随栈出栈而别销毁。
  1. 同步消除synchronization Elimination:如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。
  1. Scalar replacement:标量scalar是不可再分解的量,比如基本数据类型,聚合量Aggregate是可以在被分解的,比如java中的对象。

锁消除

当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。
例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样即时编译器(JIT)就会优化移除掉这些锁操作。
锁消除的 JVM 参数如下:
  • 开启锁消除:-XX:+EliminateLocks
  • 关闭锁消除:-XX:-EliminateLocks
锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。
使用benchmark进行性能测试,将项目打成jar包,在jar包所在根目录执行java -jar benchmarks.jar
@Fork(1) @BenchmarkMode(Mode.AverageTime) @Warmup(iterations=3) @Measurement(iterations=5) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class MyBenchmark { static int x = 0; @Benchmark public void a() throws Exception { x++; } @Benchmark // JIT 即时编译器 public void b() throws Exception { Object o = new Object(); synchronized (o) { x++; } } }
notion image
发现加锁和不加锁相差无几
 

锁粗化

对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化。 申请多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
public void doSomethingMethod(){ synchronized(lock){ //do some thing } //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕 synchronized(lock){ //do other thing } }
上面的代码是有两块需要同步操作的,但在这两块需要同步操作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗