- 同步实例方法,锁是当前实例对象
- 同步类方法,锁是当前类对象
- 同步代码块,锁是括号里面的对象
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
执行monitorexit的线程必须是对应的monitor的所有者:monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 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虚拟机的源码实现。
- 对象头:存储了比如hash、对象分代年龄、锁标志状态、偏向线程ID,偏向时间、数组长度(数组对象)。对象头一般占有2个机器码,在32位虚拟机中,1个机器码等于4个字节32bit,64位虚拟机中一个机器码8个字节64个bit。
- 实例数据:存储类的属性数据包括父类的属性信息
- 对齐填充:虚拟机要求,对象起始地址必须是8字节的整数倍。填充无数据,只是为了字节对齐和内存划分规整。
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
System.out.println(ClassLayout.parseInstance(object).toPrintable());
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
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方法,就仍然是偏向锁,可自行实验。
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
分析上面的结果,两个线程几乎同时启动竞争同一把锁,竞争到的锁的线程会休眠一段时间,这个时间远远超出了自旋的时间,可以看到锁已经升级为重量级锁。
- 如果一个对象只能被一个线程被访问到,对这个对象的操作可以不同步
- 可以将内存分配从堆分配转换成栈分配
- 分离对象或标量替换。这种适用于对象不需要在连续的内存被分配,部分属性可以在栈或者寄存器中存储
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对其做了较多的优化,为了避免直接让线程从用户态切换到内核态,从无锁、偏向锁、轻量级锁、自旋、重量级锁一步一步升级,而且编译器也通过锁消除、逃逸分析、标量替换对程序性能做了进一步提升。