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){
   
//处理相关业务
}

  1. Thread2线程执行上述代码时,当前对象this会被上一把锁,这是一把重量级锁,this对象头的Mark Word字段指向了操作系统创建的Monitor对象引用地址
  2. Monitor对象只能有一个owner,此时如果有其它线程如Thread-3或Thread-4等线程要获取这把锁就要进入Monitor对象的堵塞队列EntryList中等待Thread2释放锁。
  3. 等待锁资源被释放后,Thread-3或Thread-4会互相竞争锁资源,并不能保证谁获取到锁,最终还是有CPU来决定。
  4. Monitor对象的WaitSet存放的是,获取到锁的线程,但是由于其它一些原因导致线程进入Waiting状态,又释放了锁资源,待介绍。

注意:

  1. 上述过程只发生在synchronized 锁住同一个对象时,不同对象会关联不同的Monitor对象
  2. 不加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
	  }
 }
  1. 当代码要执行到method1()的synchronized( obj )
    此时的现象是

    操作系统为Thread-0开辟了一块栈内存,栈内存中又为synchronized( obj )开辟了一块栈帧,栈帧里面存储的是锁记录信息,它包括锁记录的地址和即将要锁对象的引用地址,这个说明它是属于哪一种锁,例如上图 00表示是一把轻量级锁。Object类则已经被new在堆中,Mark Word标识此时的状态是无锁状态 01.

  2. 执行完synchronized( obj )时后

    此时发生了两个重要变化,一是线程内部的锁记录地址和Object对象的Mark Word信息互换,二是锁记录对象的object 引用指向了Object对象的地址,这两个步骤执行后,也就完成了上锁的过程。
    其中要注意的是

    • 只有Object的Mark Word标识为无锁状态时,才可以上锁。
    • 第一步的信息交换被称为cas交换,是原子性的,要不成功要不失败。
  3. 代码继续执行,进入method2()的synchronized( obj )又会重复上面的步骤,但是此时cas就会交换失败!

    此时在栈内存中又会分配一个锁对象记录,Object 引用指向了Object对象,但此时的Object对象已经上了一把轻量级锁,于是不能再上锁了,cas交换失败,现在就处在竞争的状态,也称之锁膨胀状态(待介绍), 锁重入现象,新增增加的一条所记录做为重入的计数。

  4. 锁的释放
    解锁的时候,如果遇到锁记录的地址为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
	  }
}

对比轻量级锁,偏向锁

回忆一下对象头的格式:

一个对象创建时:

  1. 默认时开启偏向锁的,对象创建后,mark word的最后三位为101,其它的thread、epoch、age 都为 0。
  2. 偏向锁默认是延迟的,不会再程序运行时立即生效,如果想避免延迟,可以修改IDEA的启动配置,添加VM参数:XX:BiasedLockingStartupDelay=0来禁用延迟
  3. 如果对象没有开启偏向锁,那么对象创建后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,此时没有达到阈值,创建出来的对象还是待偏向状态

<mark>偏向锁的知识点到此为止。</mark>

各种锁状态标识:

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