hotspot虚拟机中,对象在内存中的布局分为三个区域
Class Metadata Address是指向类元数据的指针,JVM通过这个指针来确定这个对象是哪个类的实例。
Mark Word用于存储对象自身的运行时数据,是实现轻量级锁和偏向锁的关键。
由于对象头的信息是与自身数据无关的额外存储成本,因此,考虑到JVM的空间效率Mark Word被设计为非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态,复用自己的存储空间,如32位的JVM上,除了上图中Mark Word默认的存储结构外,还有如下变化的结构,其中轻量级锁和偏向锁是Java6后对synchronized锁进行优化后新增加的。
主要说重量级锁,也就是synchronized锁,锁的标志位为10,其中指针指向的是Monitor的起始地址,每一个对象都有一个Monitor对象与之关联。对象与Monitor的关联实现有多种方式。Monitor可以和对象一起创建和销毁,或当线程试图获取锁时自动生成。但当一个Monitor被持有后,它变处于锁定状态
每个Java对象天生自带了一把看不见的锁,叫做内部锁或Monitor锁。
可以把它理解为一个同步工具,也可以描述为一种同步机制,通常被描述为一个对象。
在hotspot虚拟机中Monitor是由ObjectMonitor来实现的,它是通过c++来实现的。
查看源码
源码地址
http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/741cd0f77fac/src/share/vm/runtime/objectMonitor.hpp
其中有等待池_WaitSet和锁池_EntryList,它们用来保存ObjectWaiter的列表,每个对象锁的线程都会被封装成ObjectWaiter来保存到里面。其中有个字段_owner,是指向持有ObjectMonitor对象的线程,当多个线程同时访问同一段同步代码时,首先会进入_EntryList中,当线程获取到对象的Monitor后,进入Object区域,并把Object的_owner设置为当前线程,其中_count就会加1,若线程调用wait方法,则释放当前持有的Monitor,_owner被恢复为null,_count减1,ObjectWaiter实例会进入到_waitSet集合中等待被唤醒。若当前线程执行完毕,将释放Monitor锁,并复位对应变量的值,以便其他线程进入获取Monitor锁。
上图即使ObjectMonitor的结构。Monitor对象存在于每个Java对象的对象头中,synchronized锁就是通过这种方式去获取锁的。这也是java对象中任意对象都可以为锁的原因。
public class SyncBlockAndMethod {
public void syncsTask(){
synchronized (this){
System.out.println("Hello");
}
}
public synchronized void syncTask(){
System.out.println("Hello again");
}
}
使用javac命令编译出.class文件
javac .java文件路径
使用javap命令反编译.class文件,查看字节码
javap -verbose .class文件路径
编译后文件
Compiled from "SyncBlockAndMethod.java"
public class com.gclhaha.javabasic.jvm.thread.SyncBlockAndMethod
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#20 // java/lang/Object."":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #26 // Hello again
#6 = Class #27 // com/gclhaha/javabasic/jvm/thread/SyncBlockAndMethod
#7 = Class #28 // java/lang/Object
#8 = Utf8
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 syncsTask
#13 = Utf8 StackMapTable
#14 = Class #27 // com/gclhaha/javabasic/jvm/thread/SyncBlockAndMethod
#15 = Class #28 // java/lang/Object
#16 = Class #29 // java/lang/Throwable
#17 = Utf8 syncTask
#18 = Utf8 SourceFile
#19 = Utf8 SyncBlockAndMethod.java
#20 = NameAndType #8:#9 // "":()V
#21 = Class #30 // java/lang/System
#22 = NameAndType #31:#32 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello
#24 = Class #33 // java/io/PrintStream
#25 = NameAndType #34:#35 // println:(Ljava/lang/String;)V
#26 = Utf8 Hello again
#27 = Utf8 com/gclhaha/javabasic/jvm/thread/SyncBlockAndMethod
#28 = Utf8 java/lang/Object
#29 = Utf8 java/lang/Throwable
#30 = Utf8 java/lang/System
#31 = Utf8 out
#32 = Utf8 Ljava/io/PrintStream;
#33 = Utf8 java/io/PrintStream
#34 = Utf8 println
#35 = Utf8 (Ljava/lang/String;)V
{
public com.gclhaha.javabasic.jvm.thread.SyncBlockAndMethod();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 7: 0
public void syncsTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Hello
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 9: 0
line 10: 4
line 11: 12
line 12: 22
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class com/gclhaha/javabasic/jvm/thread/SyncBlockAndMethod, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void syncTask();
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 #5 // String Hello again
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
}
SourceFile: "SyncBlockAndMethod.java"
其中查看syncsTask
monitorenter指向同步代码块的开始位置,首先获取PrintStream类,传入Hello,调用println打印。monitorexit指明同步代码块结束的位置。当monitorenter指令时,当前线程将试图获取对象锁,以及ObjectRef所对应的持有权,当ObjectRef的count为0时,线程就可以获取monitor,并将计数器设置为1,表示取锁成功。如果当前线程在之前已经拥有的ObjectRef的持有权,它可以重入这个monitor。
在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个方法调用synchronized方法中,在其方法内部调用另一个synchronized方法,是允许的。例如:
public void syncsTask(){
synchronized (this){
System.out.println("Hello");
synchronized (this){
System.out.println("World");
}
}
}
如果其他线程先于当前线程拥有objectRef的monitor所有权,当前线程将会阻塞在monitorEnter
这里,直到持有该锁的线程执行完毕,即monitorexit
执行,将释放monitor锁,并设置计数器为0,其他线程将有机会持有monitor。为了保证在方法异常完成时monitorexter
和monitorexit
依然可以配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可以处理所有异常,它的作用就是去执行monitorexit
指令。从字节码中也可以看出多了一个monitorexit
指令,它就是异常结束时,被释放monitor的指令。
接下来看看syncTask
方法
public synchronized void syncTask();
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 #5 // String Hello again
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
可以看到ACC_SYNCHRONIZED
访问标志来区分一个方法是不是同步方法,方法调用时,调用指令会检查ACC_SYNCHRONIZED
访问标志是否被设置,方法运行将生成monitor,无论方法正常还是异常结束,都会释放monitor,在方法执行期间,执行线程持有了monitor,其他线程无法再获得同一个monitor。如果一个同步方法运行期间抛出异常,并且在方法内部无法处理此异常,这个同步方法的所持有的monitor,将在同步方法把异常抛到同步方法外时自动释放。
在早期Java版本中,synchronized属于重量级锁,依赖Mutex Lock实现。线程之间切换需要从用户态转换到核心态,开销较大。但在Java6以后,synchronized性能得到了很大的提升。其中有:
许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得。可以让没有获取锁的线程多等待一会儿,但不放弃cpu的执行时间,这就是自旋。
通过让线程执行忙循环,等待锁的释放,不让出cpu。类似while(true)
一样去等待持有锁的线程释放锁,但又不像sleep
一样去放弃cpu的执行时间。
自旋锁在Java4就已被引入,只是当时默认是关闭的,在Java6后默认为开启状态。在本质上,自旋锁和阻塞并不相同,不考虑对多处理器的要求,如果锁占的时间非常短,那么自旋锁的性能就会很好,相反,如果锁被其他线程长时间占用,会带来许多性能上的开销。自旋始终会占用cpu的时间片,如果占用时间太长,自旋线程会白白消耗资源,所以等待的时间要有一定的限度,如果自旋超过限定的尝试次数限制后,仍然没有成功获取到锁,就应该用传统的方式去挂起线程。在jdk中,可以使用PreBlockSpin参数来更改。
自旋的次数不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程在运行中,那么JVM会认为该锁自旋获取到锁的可能性比较大,会自动增加等待时间,比如增加到50次循环。相反,如果对于某一个锁自旋很少成功获取到锁,那在以后要获取这个锁时,将可能省略掉自旋过程,以免浪费处理器资源。有了自适应自旋,JVM对程序的锁的预测会越来越精准。
JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。可以节省毫无意义的锁请求时间。
public class StringBufferWithoutSync {
public void add(String str1,String str2){
// StringBuffer是线程安全的,由于sb只会在append方法中使用,不可能被其他线程引用
// 因此sb属于不可能共享的资源,JVM会自动消除内部的锁
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 1000; i++) {
withoutSync.add("aaa","bbb");
}
}
}