1. Java对象头介绍
Java的世界里,万物皆对象,对象是类的实例,类是抽象的,对象是具体的…这些都是Java对象的一些常见介绍。
今天我们要学习的是Java对象的底层结构对象头的部分,它是Java对象的一部分,看一张HotSpot虚拟机中 Java对象的结构图:
在32位虚拟机中
普通Java对象头,占8个字节:
KlassWord (32bits):指的是该对象的类型,占4个字节
MarkWord (32bits):存放Monitor 对象的指针的引用地址,待介绍。
数组对象头,占16个字节:
与普通Java对象头不同的是数组对象头多出了数组长度字段,占4个字节
Mark Word结构:
一个通常的Java对象的Mark Word结构State处于Normal状态,即无锁状态。
状态为Biased表示该对象被加上了偏向锁,状态LightweightLocked表示该对象被加上了轻量级锁,HeavyweightLocked表示重量级锁,当对象被加上了synchronized 此时就被上了重量级锁,Mark Word的结构也将发生变化。MarkedforGC 表示被GC标记,等待回收。
64位虚拟机的Mark Word结构
2. synchronized 与 Monitor对象
Monitor被翻译成监视器或管程。每一个Java对象都可以关联一个Monitor 对象,如果使用synchronized给对象上锁(重量级锁),Mark Word位置就会指向Monitor对象的引用地址。
当我们线程2执行如下代码时:
synchronized(this){
//处理相关业务
}
- Thread2线程执行上述代码时,当前对象this会被上一把锁,这是一把重量级锁,this对象头的Mark Word字段指向了操作系统创建的Monitor对象引用地址
- Monitor对象只能有一个owner,此时如果有其它线程如Thread-3或Thread-4等线程要获取这把锁就要进入Monitor对象的堵塞队列EntryList中等待Thread2释放锁。
- 等待锁资源被释放后,Thread-3或Thread-4会互相竞争锁资源,并不能保证谁获取到锁,最终还是有CPU来决定。
- Monitor对象的WaitSet存放的是,获取到锁的线程,但是由于其它一些原因导致线程进入Waiting状态,又释放了锁资源,待介绍。
注意:
- 上述过程只发生在synchronized 锁住同一个对象时,不同对象会关联不同的Monitor对象
- 不加synchronized 的对象不会关联监视器Monitor,也就不会发生上述过程
3. synchronized 原理总结
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
3.1 字节码角度理解synchronized原理
从字节码角度理解上诉代码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)
V flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // <- lock引用(synchronized开始)
3: dup //对象复制一份
4: astore_1 // 复制完的对象存储一份 -> slot 1
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // <- 取的i 6-11做自加操作
9: iconst_1 // 准备常数 1
10: iadd // +1
11: putstatic #3 // -> i
14: aload_1 // <- lock引用到刚刚复制的那一份锁对象的地址
15: monitorexit // 将 lock对象 MarkWord 重置(释放锁), 唤醒 EntryList
16: goto 24 //去执行24行
19: astore_2 // e -> slot 2 (异常情况处理开始)
20: aload_1 // <- lock引用 刚刚复制的那一份锁对象的地址
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 // <- slot 2 (e) 获取异常对象
23: athrow // throw e 抛出
24: return
Exceptiontable:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries=2
frame_type=255/* full_frame */
offset_delta=19
locals= [ class"[Ljava/lang/String;", classjava/lang/Object ]
stack= [ classjava/lang/Throwable ]
frame_type=250/* chop */
offset_delta=4
3.2 synchronized进阶原理
3.2.1 synchronized轻量级锁
轻量级锁也是synchronized 实现的,只不过适用的场景与重量级有所不同,通俗一点说,就是轻量级锁适用于重量级锁可以适当优化的地方,如:线程之间没有发生竞争,即上锁的对象,线程1解锁的时间和线程2上锁的时间是错开的,这个时候可以使用轻量级锁优化。
假设又两个方法的同步代码块。利用同一对象上锁:
static final Object obj=new Object();
public static void method1()
{
synchronized( obj )
{
// 同步块
A method2();
}
}
public static void method2()
{
synchronized( obj )
{
// 同步块 B
}
}
-
当代码要执行到method1()的
synchronized( obj )
时
此时的现象是
操作系统为Thread-0开辟了一块栈内存,栈内存中又为synchronized( obj )
开辟了一块栈帧,栈帧里面存储的是锁记录信息,它包括锁记录的地址和即将要锁对象的引用地址,这个说明它是属于哪一种锁,例如上图 00表示是一把轻量级锁。Object类则已经被new在堆中,Mark Word标识此时的状态是无锁状态 01. -
执行完
synchronized( obj )
时后
此时发生了两个重要变化,一是线程内部的锁记录地址和Object对象的Mark Word信息互换,二是锁记录对象的object 引用指向了Object对象的地址,这两个步骤执行后,也就完成了上锁的过程。
其中要注意的是:- 只有Object的Mark Word标识为无锁状态时,才可以上锁。
- 第一步的信息交换被称为cas交换,是原子性的,要不成功要不失败。
-
代码继续执行,进入method2()的
synchronized( obj )
又会重复上面的步骤,但是此时cas就会交换失败!
此时在栈内存中又会分配一个锁对象记录,Object 引用指向了Object对象,但此时的Object对象已经上了一把轻量级锁,于是不能再上锁了,cas交换失败,现在就处在竞争的状态,也称之锁膨胀状态(待介绍), 锁重入现象,新增增加的一条所记录做为重入的计数。 -
锁的释放
解锁的时候,如果遇到锁记录的地址为null
,则直接释放掉,删除锁记录。如果不为null
,则需要将上锁时候的信息交换重置回来,恢复对象头的Mark Word信息。
如果成功,则表示解锁成功。
如果失败说明轻量级锁经过了锁膨胀变成重量级锁,则需要进行重量级锁的解锁过程。
3.2.2 synchronized锁膨胀
如果cas操作失败,并且有其它线程为该对象上锁了,那个其它线程上的如果是轻量级锁,此时就有竞争条件了,会发生锁膨胀,轻量级锁就加不上了,需要升级为重量级锁。
升级为重量级🔒
当Thread -1上锁失败,就会为Object对象申请Monitor锁,让Object的Mark Word重新指向Monitor对象,Thread-1则会进入Monitor的Entry的队列中堵塞。
细节发生的变化有,Object 对象头的Mark Word指向了Monitor对象,指向之前会先得到Monitor对象,然后将其Owner改为Thread-0的锁记录拥有,因为此时Thread-0的锁还未释放。
解锁过程:
Thread-0如果要解锁,如果走的轻量级的解锁流程会失败,则走的就是重量级锁的结果流程,即把Monitor对象的Owner置为null,并且去唤醒Entry List堵塞的线程去竞争。
3.2.3 synchronized自旋锁
重量级锁竞争锁资源的时候,还可以通过自旋来进行优化,场景发生在:当锁资源被占用的情况下,Monitor对象中的Entry List线程不用马上进入堵塞队列,而是进入自旋状态,简单可以理解为在做循环试探锁资源是否被释放了,目的是达到锁资源一释放就可以立马被下一个线程使用,不要再去进行唤醒操作。
但是要注意的是:
- 自旋会占用CPU的资源,如果是单核CPU就会存在很大的浪费,所以自旋使用与多核的CPU.
- Java 7之后就不能手动控制是否开启自旋功能了,而是由JVM自动执行,并且是自适应的,例如如果一次自旋成功,就会被认为自旋成功的可能性大,就会多自旋几次,反之,少自旋或者不自旋,设计的比较智能。
3.2.4 synchronized偏向锁(重难点)
偏向锁是对轻量级锁的再次优化,体现在减少cas的次数,因为我们在对轻量级上锁的过程中,会遇到前面我们谈论的一种情况:
第一次对Object对象上锁的过程中会有一次cas操作,如果要对Object对象第二次上锁,则会cas失败,此时锁记录指针会指向null,并且操作系统会创建一个新的栈帧存储这一个锁记录,依次类推,如果这个Object对象被重复n次,则会生成n个这样的记录,作为锁的可重入的计数。
这样就会存在一个问题:
如果产生了n次这样的计数,则会进行n次的cas操作,这样是很耗CPU资源的,所以可以使用偏向锁进行优化:第一次进行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
}
}
对比轻量级锁,偏向锁
回忆一下对象头的格式:
一个对象创建时:
- 默认时开启偏向锁的,对象创建后,mark word的最后三位为101,其它的thread、epoch、age 都为 0。
- 偏向锁默认是延迟的,不会再程序运行时立即生效,如果想避免延迟,可以修改IDEA的启动配置,添加VM参数:XX:BiasedLockingStartupDelay=0来禁用延迟
- 如果对象没有开启偏向锁,那么对象创建后mark word的最后三位为001,这时它的hashcode,age…都等于0,这又用到这些字段的时候才会被赋值。
偏向锁测试:
我们可以借助第三方jar报来测试偏向锁,创建对象对象头默认会加上偏向锁,引入jol依赖
</dependency>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
- 测试偏向锁的启动延迟性
public static void main(String[] args) {
Dog dog = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(dog);
System.out.println(classLayout.toPrintable());
}
输出:
当我们让主线程睡5秒,再测试
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Dog dog = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(dog);
System.out.println(classLayout.toPrintable());
}
输出:
可以观察到,后三位已经被加上偏向锁状态位。需要注意的是:Thread.sleep(5000);不能写在Dog dog = new Dog();后面,因为偏向锁的延迟性是体现在将要创建对象的时候。
- 另外一种展示偏向锁的方法,禁用偏向锁延迟,这样就可以在主线程不用睡眠的情况下,给对象加上偏向锁,操作方法如下:
修改启动配置:给VM加上-XX:BiasedLockingStartupDelay=0
重新跑一下程序,输出:
观察synchronized
与偏向锁的结合前后对象头的变化:
public static void main(String[] args) {
Dog dog = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(dog);
new Thread(()->{
log.debug("加锁前....");
log.debug(classLayout.toPrintable());
synchronized (dog){
log.debug("加锁中....");
log.debug(classLayout.toPrintable());
}
log.debug("解锁后....");
log.debug(classLayout.toPrintable());
},"t1").start();
}
输出:
- 禁用偏向锁,配置启动
-XX:-UseBiasedLocking
,启动测试
该现象说明我们禁用掉偏向锁后,会优先启动轻量级锁,而不是重量级锁。
偏向锁被撤销情况:
情况1:
- 再测试一个有趣的现象,如果我们调用对象的hashcode,则会自动禁用掉偏向锁,首先我们开启偏向锁,使用
-XX:BiasedLockingStartupDelay=0
来禁用延迟
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(dog);
log.debug(classLayout.toPrintable());
log.debug("hashcode前....");
System.out.println(dog.hashCode());
log.debug("hashcode前....");
new Thread(()->{
log.debug("加锁前....");
log.debug(classLayout.toPrintable());
synchronized (dog){
log.debug("加锁中....");
log.debug(classLayout.toPrintable());
}
log.debug("解锁后....");
log.debug(classLayout.toPrintable());
},"t1").start();
// System.out.println(classLayout.toPrintable());
}
猜想对象头的锁状态
1. 偏向锁(101)
2. 无锁(001)
3. 轻量级锁(000)
4. 无锁(001)
输出:
其中补充一点的是我们的对象hashcode值并不是在创建对象时生成的,而是在第一次调用hashcode()
时生成的!
Dog dog = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(dog);
log.debug(classLayout.toPrintable());
System.out.println(dog.hashCode());
log.debug(classLayout.toPrintable());
输出:
情况2:演示线程1和线程2,对对象上锁解锁互不影响,没有竞争,此时的t2上锁后,就会把t1的偏向锁撤销掉,换成轻量级锁,原来的偏向锁的线程id也会被换成锁记录。
Dog dog = new Dog();
ClassLayout classLayout = ClassLayout.parseInstance(dog);
new Thread(()->{
synchronized (dog){
log.debug("t1加锁中....");
log.debug(classLayout.toPrintable());
}
//t1解锁后唤醒类锁,使得t2可以继续执行下去
synchronized (Test.class){
Test.class.notify();
}
},"t1").start();
new Thread(()->{
//等待t1释放锁
synchronized (Test.class){
try {
Test.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("t2加锁前....");
log.debug(classLayout.toPrintable());
synchronized (dog){
log.debug("t2加锁中....");
log.debug(classLayout.toPrintable());
}
log.debug("t2解锁后....");
log.debug(classLayout.toPrintable());
},"t2").start();
输出:
15:51:26.521 c.TestBiased [t1] - t1加锁中....
15:51:26.525 c.TestBiased [t1] - com.liuzeyu.testsynchronized.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0d 98 22 dc (00001101 10011000 00100010 11011100) (-601712627)
4 4 (object header) df 01 00 00 (11011111 00000001 00000000 00000000) (479)
8 4 (object header) 7d 44 01 08 (01111101 01000100 00000001 00001000) (134300797)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
15:51:26.525 c.TestBiased [t2] - t2加锁前....
15:51:26.527 c.TestBiased [t2] - com.liuzeyu.testsynchronized.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0d 98 22 dc (00001101 10011000 00100010 11011100) (-601712627)
4 4 (object header) df 01 00 00 (11011111 00000001 00000000 00000000) (479)
8 4 (object header) 7d 44 01 08 (01111101 01000100 00000001 00001000) (134300797)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
15:51:26.527 c.TestBiased [t2] - t2加锁中....
15:51:26.528 c.TestBiased [t2] - com.liuzeyu.testsynchronized.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 60 f1 1f 31 (01100000 11110001 00011111 00110001) (824176992)
4 4 (object header) 9d 00 00 00 (10011101 00000000 00000000 00000000) (157)
8 4 (object header) 7d 44 01 08 (01111101 01000100 00000001 00001000) (134300797)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
15:51:26.528 c.TestBiased [t2] - t2解锁后....
15:51:26.530 c.TestBiased [t2] - com.liuzeyu.testsynchronized.Dog object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) (9)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 7d 44 01 08 (01111101 01000100 00000001 00001000) (134300797)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
情况3:
我们调用对象wait()/notify()
方法时,偏向锁也会升级为重量级锁,为什么不是轻量级锁呢,因为wait()/notify()
属于Object对象的方法,所以调用该方法后,锁会升级为重量级锁。
在没有禁用偏向锁延迟的情况下
Dog d = new Dog();
new Thread(() -> {
log.debug("上锁前....");
log.debug(ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug("wait()前....");
log.debug(ClassLayout.parseInstance(d).toPrintable());
try {
d.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("wait()后....");
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
}, "t1").start();
new Thread(() -> {
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (d) {
log.debug("notify");
d.notify();
}
}, "t2").start();
输出:
另外一种情况:
我们前面演示了多线程情况下,对对象的加锁操作,没有发生竞争的前提下。当发生多次的偏向锁撤销时,JVM底层就会自己做处理,也就是不撤销,因为撤销进行锁升级也是很耗内存的。
例如下面例子:先把对象以偏向锁的状态放入集合中存储起来,然后再拿出来加锁,根据上面的规则,此时偏向锁会被撤销掉升级为轻量级锁,并且线程id也会被锁记录替换掉。
但是JVM会认为撤销的阈值达到20时,该对象就会重新偏向新的加锁线程,也就是偏向线程t2,这是一块比较细节的知识点。
public static void main(String[] args) throws InterruptedException {
Vector<Dog> list=new Vector<>();
new Thread(() -> {
for (int i = 0; i < 30; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug("t1加锁中..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}
synchronized (list) {
list.notify();
}
}, "t1").start();
new Thread(() -> {
//等待
synchronized (list) {
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 30; i++) {
Dog d = list.get(i);
log.debug("t2加锁前..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug("t2加锁中..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug("t2解锁后..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}, "t2").start();
}
观察结果:
由于输出的内容比较多,这边只展示重点部分:
t1线程 0-29次输出:
t2线程 0-18次输出:
当t2线程达到阈值20次的时候,也就是19-29的输出就会变化:
最后一种特殊的情况:当撤销偏向锁的偏向的阈值达到40时,再创建新的对象,它都是不可偏向的,也就是无锁状态。
@Slf4j(topic = "c.TestBiased")
public class TestBiased {
static Thread t1,t2,t3;
public static void main(String[] args) throws InterruptedException {
int loopNumber = 39;
Vector<Dog> list=new Vector<>();
t1 = new Thread(() -> {
for (int i = 0; i < loopNumber; i++) {
Dog d = new Dog();
list.add(d);
synchronized (d) {
log.debug("t1加锁中..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}
LockSupport.unpark(t2);
}, "t1");
t1.start();
t2 = new Thread(() -> {
//等待
LockSupport.park();
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug("t2加锁前..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug("t2加锁中..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug("t2解锁后..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
LockSupport.unpark(t3);
}, "t2");
t2.start();
t3 = new Thread(() -> {
//等待
LockSupport.park();
for (int i = 0; i < loopNumber; i++) {
Dog d = list.get(i);
log.debug("t3加锁前..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
synchronized (d) {
log.debug("t3加锁中..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
log.debug("t3解锁后..");
log.debug(i + "\t" + ClassLayout.parseInstance(d).toPrintable());
}
}, "t3");
t3.start();
t3.join();
log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
}
}
观察结果:
线程t1:0-38次,将对象加上偏向锁并放入集合
线程t2:0-18次,将集合中的0-18个元素的偏向锁撤销为无状态锁,19-18次批量重偏向到t2线程
t3:0-18次,将集合中的0-18个元素的无状态锁重新撤销,然后上轻量级锁,之后解锁。19-38次,偏向t2的偏向锁再次撤销。
因此批量锁在t3线程被撤消了39次,所以我们等到t3线程执行完,再次去创建新的对象,此时达到阈值40,创建的对象都是无锁状态了!!
如果将loopNumber 改成38,此时没有达到阈值,创建出来的对象还是待偏向状态
偏向锁的知识点到此为止。
各种锁状态标识:
3.2.5 synchronized锁消除
分析以下代码:a(),b()方法哪个运行效率更高?
@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++;
}
}
}
D:\BaiduNetdiskDownload\\concurrent\jmh_eliminate_locks\target>java -jar benchmarks.jar
# VM invoker: C:\Program Files\Java\jre1.8.0_91\bin\java.exe
# VM options: <none>
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.itcast.MyBenchmark.a
# Run progress: 0.00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration 1: 2.623 ns/op
# Warmup Iteration 2: 3.099 ns/op
# Warmup Iteration 3: 2.528 ns/op
Iteration 1: 2.989 ns/op
Iteration 2: 3.323 ns/op
Iteration 3: 3.105 ns/op
Iteration 4: 4.456 ns/op
Iteration 5: 4.371 ns/op
Result: 3.649 ±(99.9%) 2.730 ns/op [Average]
Statistics: (min, avg, max) = (2.989, 3.649, 4.456), stdev = 0.709
Confidence interval (99.9%): [0.919, 6.378]
# VM invoker: C:\Program Files\Java\jre1.8.0_91\bin\java.exe
# VM options: <none>
# Warmup: 3 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.itcast.MyBenchmark.b
# Run progress: 50.00% complete, ETA 00:00:10
# Fork: 1 of 1
# Warmup Iteration 1: 4.137 ns/op
# Warmup Iteration 2: 5.132 ns/op
# Warmup Iteration 3: 5.046 ns/op
Iteration 1: 5.145 ns/op
Iteration 2: 4.631 ns/op
Iteration 3: 4.458 ns/op
Iteration 4: 5.114 ns/op
Iteration 5: 4.205 ns/op
Result: 4.711 ±(99.9%) 1.585 ns/op [Average]
Statistics: (min, avg, max) = (4.205, 4.711, 5.145), stdev = 0.412
Confidence interval (99.9%): [3.126, 6.295]
# Run complete. Total time: 00:00:20
Benchmark Mode Samples Score Score error Units
c.i.MyBenchmark.a avgt 5 3.649 2.730 ns/op
c.i.MyBenchmark.b avgt 5 4.711 1.585 ns/op
从最终得分可以看出来a()方法还是要更高效一点,因为b()方法加上了synchronized
关键字,过程会存在一定的内存损耗,但是由于JVM默认是开启JIT即时编译器的,而且b()可以看出,局部变量o 的作用域只是在方法内部,也就是说处于线程安全状态下,这边JIT内部会做synchronized锁的清除操作,目的也就是提高代码执行的效率。
我们也可以手动关掉JIT,再次查看执行情况:
可以没有优化之前加上synchronized锁的效率是很慢的。
学习资料:https://www.bilibili.com/video/BV16J411h7Rd