synchronized原理

如何解决线程并发安全问题?
并发场景下解决线程安全问题的方案都是使多线程序列化访问临界资源。即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock。synchronized是java的一个关键字,是JVM内置的锁,而Lock是并发包下的一种锁工具。
synchronized内置锁是一种对象锁,锁的是对象而非引用,作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入锁。
  • 同步实例方法,锁是当前实例对象
  • 同步类方法,锁是当前类对象
  • 同步代码块,锁是括号里面的对象
synchronized底层原理
synchronized基于JVM内置锁实现,基于进入和退出Monitor(监视器锁)对象实现方法和代码块同步。监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁),这是一种重量级锁,性能比较低。1.5之后版本对synchronized做了的优化,如锁粗化、锁消除、轻量级锁、偏向锁、自旋等来减少锁操作的开销,可以认为synchronized的并发性能已经基本与Lock持平。
synchronized关键字被编译成字节码后会被翻译成monitorenter和monitorexit两条指令分别在同步块逻辑代码的起始位置与结束位置。
Monitor监视器锁:任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,通过成对的MonitorEnter和MonitorExit指令来实现。
线程执行monitorenter尝试获取monitor所有权:
  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

执行monitorexit的线程必须是对应的monitor的所有者:monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

从上面可以知道synchronized是依赖monitor对象的,包括wait/notify等方法也依赖monitor,所以只有在同步块或者方法中才能调用。
 
看一个同步方法和非同步方法:
public class SynchronizedMethod {
    public synchronized void synchronizedMethod() {
        System.out.println("synchronized method");
    }

    public void normalMethod() {
        System.out.println("normal method");
    }
}

执行两条查看反编译的结果:

javac SynchronizedMethod.java
javap -v SynchronizedMethod.class
 public synchronized void synchronizedMethod();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String synchronized method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0
        line 14: 8

  public void normalMethod();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String normal method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 17: 0
        line 18: 8

第一个方法从反编译的结果来看,并没有monitorenter和monitorexit,不过相对第二个方法,多了ACC_SYNCHRONIZED标识符。方法调用时,会检查访问标志是否设置了ACC_SYNCHRONIZED标识符,如果设置了,线程将获取monitor,成功之后才能执行方法体,执行完成之后释放monitor,在此期间其他线程不能获得同一个monitor对象。两种方式本质上没有区别,这是一种隐式的实现,不需要通过字节码来完成。两个指令的执行是JVM调用操作系统的互斥原语mutex来实现的,被阻塞的线程会挂起,等待重新调度,且会导致线程在用户态和内核态之间切换,对性能有一定影响。

monitor,可以理解为一个同步工具,被描述为一个对象。所有的java对象都具备monitor的性质,深入了解的话要去看HotSpot虚拟机的源码实现。

那么现在有个问题,synchronized是加在对象上的,对象又是如何记录锁状态的?这要从对象的内存布局开始看起。对象在内存中的存储划分为三块区域:对象头、实例数据和对齐填充
  • 对象头:存储了比如hash、对象分代年龄、锁标志状态、偏向线程ID,偏向时间、数组长度(数组对象)。对象头一般占有2个机器码,在32位虚拟机中,1个机器码等于4个字节32bit,64位虚拟机中一个机器码8个字节64个bit。
  • 实例数据:存储类的属性数据包括父类的属性信息
  • 对齐填充:虚拟机要求,对象起始地址必须是8字节的整数倍。填充无数据,只是为了字节对齐和内存划分规整。
对象头
第一部分是Mark Word,用于存储对象自身的运行时数据, 如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,是实现轻量级锁和偏向锁的关键。这部分数据的长度在32位和64位的虚拟机中分别为32个和64个Bits(不考虑指针压缩)。对象需要存储的运行时数据很多,其实已经超出了32、64位Bitmap结构所能记录的限度,但是对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0。
第二部分是Klass Pointer类型指针,指向类的元数据。
第三部分是数组对象才有,用一块内存来记录数组长度。
下图展示的是32位虚拟机的java对象头数据存储:
 
现在我们虚拟机基本是64位的,而64位的对象头有点浪费空间,JVM默认会开启指针压缩,所以基本上也是按32位的形式记录对象头的。手动开启指针压缩:-XX:+UseCompressedOops
对象头分析工具:OpenJDK开源工具包,引入下方maven依赖
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>
System.out.println(ClassLayout.parseInstance(object).toPrintable());
锁的膨胀升级过程
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,不能降级。默认开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking来禁用偏向锁。
偏向锁是一种针对加锁操作的优化手段,在大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,这样就省去一些有关锁申请的操作,相对来说性能更高。所以对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失。偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0,默认就是开启的
关闭偏向锁:-XX:-UseBiasedLocking
看一个偏向锁的示例:
public static void main(String[] args) throws InterruptedException {
        // 休眠五秒钟,启动偏向锁。这是因为jvm启动的时候,内部会有大量的同步块,如果这时启动偏向锁,会产生很多的无意义的竞争,这是延迟启动的
        TimeUnit.SECONDS.sleep(5);
        Object lock = new Object();
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      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

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 80 67 (00000101 00000000 10000000 01100111) (1736441861)
      4     4        (object header)                           dc 7f 00 00 (11011100 01111111 00000000 00000000) (32732)
      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
分析结果,注意我们的机器是小端存储,VALUE要反过来看。
看第一个标红的位置,发现已经启动了偏向锁,但是后面并没有记录任何线程id之类的信息,这叫做匿名偏向,表示的是现在没有偏向,但是随时可以进行偏向。
加了synchronized关键字之后,现在的锁标志位是偏向锁,后面已经记录了相关偏向的信息。
轻量级锁:倘若偏向锁失败,并不会立即升级为重量级锁,会尝试使用一种称为轻量级锁的优化手段,此时Mark Word的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,不过这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
看一个偏向锁升级为轻量级锁的demo:
public static void main(String[] args) throws InterruptedException {
        // 休眠五秒钟启动偏向锁
        Thread.sleep(5000);
        Object lock = new Object();
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        new Thread(() -> {
            synchronized (lock) {
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            }
        }).start();
        Thread.sleep(2000);
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        new Thread(() -> {
            synchronized (lock) {
                System.out.println(ClassLayout.parseInstance(lock).toPrintable());
            }
        }).start();
    }
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      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

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 e8 93 6e (00000101 11101000 10010011 01101110) (1855186949)
      4     4        (object header)                           f7 7f 00 00 (11110111 01111111 00000000 00000000) (32759)
      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

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 e8 93 6e (00000101 11101000 10010011 01101110) (1855186949)
      4     4        (object header)                           f7 7f 00 00 (11110111 01111111 00000000 00000000) (32759)
      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

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           68 78 74 05 (01101000 01111000 01110100 00000101) (91519080)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      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

分析输出结果,看对应的四个输出位,第一次无锁,匿名偏向,启动一个线程加了synchronized之后,偏向当前线程,休眠两秒后,依然是偏向锁偏向当前线程,输出和上面没有任何改变,再开启一个线程竞争锁,可以看到锁标记为00已经升级为轻量级锁。

还有个一点值得注意的是,如果对象调用了hashCode方法,会由偏向锁升级为轻量级锁,从对象内存布局中我们可以看到,无锁状态会存储hashCode、轻量级锁、重量级锁都有一块指针指向某处内存,而偏向锁记录了偏向线程id,并没有存储hashCode的地方,这可能是其升级的原因,看下面的demo验证一下。

public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Lock lock = new Lock();
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
        // 输出hashcode
        System.out.println(lock.hashCode());
        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }

class Lock {
    int    a = 0;
    String b = "123";
}
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4                int Lock.a                                    0
     16     4   java.lang.String Lock.b                                    (object)
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

com.dluo.Lock object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           05 40 80 4f (00000101 01000000 10000000 01001111) (1333805061)
      4     4                    (object header)                           96 7f 00 00 (10010110 01111111 00000000 00000000) (32662)
      8     4                    (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4                int Lock.a                                    0
     16     4   java.lang.String Lock.b                                    (object)
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

1612799726
com.dluo.Lock object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           e8 68 20 08 (11101000 01101000 00100000 00001000) (136341736)
      4     4                    (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4                    (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4                int Lock.a                                    0
     16     4   java.lang.String Lock.b                                    (object)
     20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

输出hashCode之后,同样的线程再次获取锁,发现锁标志位已经编程了轻量级锁,如果不调用hashCode方法,就仍然是偏向锁,可自行实验。

自旋锁:轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
模拟竞争激烈的环境,查看重量级锁:
public static void main(String[] args) throws InterruptedException {
        Thread.sleep(5000);
        Object lock = new Object();
        Thread thread1 = new Thread() {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                    Thread.sleep(2000);
                }
            }
        };
        Thread thread2 = new Thread() {
            @SneakyThrows
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println(ClassLayout.parseInstance(lock).toPrintable());
                    Thread.sleep(2000);
                }
            }
        };
        thread1.start();
        thread2.start();
    }
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           0a 4c 01 f2 (00001010 01001100 00000001 11110010) (-234796022)
      4     4        (object header)                           ba 7f 00 00 (10111010 01111111 00000000 00000000) (32698)
      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

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           0a 4c 01 f2 (00001010 01001100 00000001 11110010) (-234796022)
      4     4        (object header)                           ba 7f 00 00 (10111010 01111111 00000000 00000000) (32698)
      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

分析上面的结果,两个线程几乎同时启动竞争同一把锁,竞争到的锁的线程会休眠一段时间,这个时间远远超出了自旋的时间,可以看到锁已经升级为重量级锁。

锁消除:另外一种锁的优化,虚拟机编译时通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。锁消除必须开启逃逸分析。
-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+EliminateLocks 表示开启锁消除
逃逸分析:
  • 如果一个对象只能被一个线程被访问到,对这个对象的操作可以不同步
  • 可以将内存分配从堆分配转换成栈分配
  • 分离对象或标量替换。这种适用于对象不需要在连续的内存被分配,部分属性可以在栈或者寄存器中存储
开启逃逸分析: -XX:+DoEscapeAnalysis,关闭逃逸分析 -XX:-DoEscapeAnalysis,默认开启逃逸分析。
来看一个demo,创建百万个User对象,打印执行时间:
public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }

    private static void alloc() {
        User user = new User();
        user.setId(1L);
        user.setName("xman");
    }
}

JVM参数,只分配少量的堆栈内存:

关闭逃逸分析,打印gc日志,-Xmx20m -Xms20m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

可以看到执行结果,做了大量的gc

[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0032854 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0012397 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0007894 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0011907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0012266 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0019541 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0018475 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0008486 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0007847 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5632K->0K(6144K)] 6306K->674K(19968K), 0.0008531 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
1018
Heap
 PSYoungGen      total 6144K, used 676K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 12% used [0x00000007bf980000,0x00000007bfa29098,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 13824K, used 674K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
  object space 13824K, 4% used [0x00000007bec00000,0x00000007beca8ad8,0x00000007bf980000)
 Metaspace       used 3101K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 340K, capacity 388K, committed 512K, reserved 1048576K

开启逃逸分析,打印gc日志,-Xmx20m -Xms20m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

输出结果并没有做gc

7
Heap
 PSYoungGen      total 6144K, used 4109K [0x00000007bf980000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 5632K, 72% used [0x00000007bf980000,0x00000007bfd83540,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
 ParOldGen       total 13824K, used 0K [0x00000007bec00000, 0x00000007bf980000, 0x00000007bf980000)
  object space 13824K, 0% used [0x00000007bec00000,0x00000007bec00000,0x00000007bf980000)
 Metaspace       used 3116K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 340K, capacity 388K, committed 512K, reserved 1048576K

总结下来的synchronized锁是基于进出monitor对象来实现的,而且现在jdk对其做了较多的优化,为了避免直接让线程从用户态切换到内核态,从无锁、偏向锁、轻量级锁、自旋、重量级锁一步一步升级,而且编译器也通过锁消除、逃逸分析、标量替换对程序性能做了进一步提升。

 
 

原文地址:https://www.cnblogs.com/dlcode/p/14172350.html