在Java中,有一个与锁有关的关键字synchronized
,它是由JVM层面提供的Monitor
来实现。
1. 语法
synchronized
关键字在语法层面1有两种形式。请看如下的示例代码:
import java.io.File;
public class SynchronizedExample {
public void instanceSynchronizedStatementMethod(Object syncObject) {
synchronized (syncObject) {
instanceMethod();
}
}
public synchronized void synchronizedInstanceMethod() {
instanceMethod();
}
public void instanceMethod() {
}
public static synchronized void synchronizedStaticMethod() {
staticMethod();
}
public static void staticMethod() {
}
}
1.1 修饰代码快
示例中的instanceSynchronizedStatementMethod
方法,这种形式的语法要求synchronized
关键字关联一个对象作为同步资源,然后紧跟着是一个代码块。可以保证在一个JVM进程内,多个使用syncObject
作为同步资源对象的线程同时访问这个代码块的时候,只能串行的进行访问,也就是同一时刻,只会有一个线程成功的进入这个代码块执行代码。当然这个同步资源对象syncObject
也可以是一个字段,实例的或者静态的都可以,包裹这个同步代码块的方法同样的也是实例的或者静态的都可以。
这种语法形式的关键在于多个线程持有的syncObject
是不是同一个,同一个的为同一个阻塞队列。
由此就可以推导出来,Java中的int
、byte
和char
等等这些基本的原始类型是不能作为syncObject
的,因为它们直接存储的是值,而不是引用;其次Integer
这种包装类型由于存在装箱拆箱,也是不可以的;再次String
由于有字符串驻留池的存在,也无法确保syncObject
不会出现错乱。
故而最好的方式就是new Object()
即可,当然你也不能有10个线程,每个线程都new
一个自己的syncObject
,而是让需要同步的那些个线程使用同一个syncObject
即可。
当你需要在整个JVM内同步所有线程时,选一个JVM内单例的syncObject
即可,比如一个静态的字段,或者一个类型的class
对象。
1.2 修饰方法
示例中的synchronizedInstanceMethod
和synchronizedStaticMethod
都是方法级别的语法。其差异在于前者是锁的this
对象,后者锁的是SynchronizedExample.class
这个对象。由于后者在一个JVM进程内是唯一的,故而相当于会影响所有的访问这个方法的线程。
2 字节码
通过字节码来看一下编译后的代码。
public class SynchronizedExample
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#22 // java/lang/Object."<init>":()V
#2 = Methodref #4.#23 // SynchronizedExample.instanceMethod:()V
#3 = Methodref #4.#24 // SynchronizedExample.staticMethod:()V
#4 = Class #25 // SynchronizedExample
#5 = Class #26 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 instanceSynchronizedStatementMethod
#11 = Utf8 (Ljava/lang/Object;)V
#12 = Utf8 StackMapTable
#13 = Class #25 // SynchronizedExample
#14 = Class #26 // java/lang/Object
#15 = Class #27 // java/lang/Throwable
#16 = Utf8 synchronizedInstanceMethod
#17 = Utf8 instanceMethod
#18 = Utf8 synchronizedStaticMethod
#19 = Utf8 staticMethod
#20 = Utf8 SourceFile
#21 = Utf8 SynchronizedExample.java
#22 = NameAndType #6:#7 // "<init>":()V
#23 = NameAndType #17:#7 // instanceMethod:()V
#24 = NameAndType #19:#7 // staticMethod:()V
#25 = Utf8 SynchronizedExample
#26 = Utf8 java/lang/Object
#27 = Utf8 java/lang/Throwable
{
public SynchronizedExample();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public void instanceSynchronizedStatementMethod(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: aload_0
5: invokevirtual #2 // Method instanceMethod:()V
8: aload_2
9: monitorexit
10: goto 18
13: astore_3
14: aload_2
15: monitorexit
16: aload_3
17: athrow
18: return
Exception table:
from to target type
4 10 13 any
13 16 13 any
LineNumberTable:
line 6: 0
line 7: 4
line 8: 8
line 9: 18
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class SynchronizedExample, class java/lang/Object, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void synchronizedInstanceMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method instanceMethod:()V
4: return
LineNumberTable:
line 12: 0
line 13: 4
public void instanceMethod();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 16: 0
public static synchronized void synchronizedStaticMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: invokestatic #3 // Method staticMethod:()V
3: return
LineNumberTable:
line 19: 0
line 20: 3
public static void staticMethod();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 23: 0
}
SourceFile: "SynchronizedExample.java"
可以看出instanceSynchronizedStatementMethod
比instanceMethod
中多来很多的指令,主要是monitorenter
和monitorexit
这两个,前者代表加锁,后者代表释放锁,由于不知掉内部会不会抛出异常,故而编译器自动添加来finaly
块来保证锁的释放。
而synchronizedInstanceMethod
和synchronizedStaticMethod
两个方法就比较简单了,只是增加来一个标记ACC_SYNCHRONIZED
。当JVM遇见这个标记的方法时,会使用和上面一样的monitorenter
和monitorexit
一样的方式来执行加锁和解锁行为。
3 实现原理
JVM底层是依赖Java的对象头中的Mark Word
和Monitor
来实现的synchronized
。
3.1 Mark Word
锁的四种状态体现在下面的表格中。
State | Mark Word (32 bit) | |||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Octet 1 | Octet 2 | Octet 3 | Octet 4 | |||||||||||||||||||||||||||||
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
23 bit | 2 bit | 4 bit | Is Biased Lock | Flag | ||||||||||||||||||||||||||||
unlocked | identity hashcode | age | 0 | 0 | 1 | |||||||||||||||||||||||||||
biased lock | thread id | epoch | age | 1 | 0 | 1 | ||||||||||||||||||||||||||
lightweight lock | pointer to stack lock record | 0 | 0 | |||||||||||||||||||||||||||||
heavyweight lock | pointer to monitor | 1 | 0 | |||||||||||||||||||||||||||||
marked for gc | 1 | 1 |
3.2 Monitor
我们通过上述的字节码已经得知了monitorenter
和monitorexit
指令以及附加到方法上的ACC_SYNCHRONIZED
标记。在文章开头提到synchronized
是由JVM层面提供的Monitor
来实现,那么这些指令和标记就是为Monitor而准备的,在JVM中通过C++实现的ObjectMonitor
2来提供支持。
java.lang.Object
的wait
、notify
和notifyAll
这些native方法也是由ObjectMonitor
实现的。
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
这里由几个重要的字段:
_owner
:拥有当前对象的线程地址。_WaitSet
:存放调用wait方法后进入等待状态的线程的队列。_EntryList
:等待锁block状态的线程的队列。_recursions
:锁的重入次数。_count
:线程获取锁的次数。
3.3 Heavyweight Lock
Jdk1.6之前,synchronized
在JVM底层就只是传统意义上的依赖OS内核mutex结合Mark Word
和Monitor
实现的传统意义上的锁,现在称之为重量级锁。
在上面的Mark Word
表格中,当Flag
是10
时,代表前面的30bit是指向Monitor对象的指针。
3.4 Lightweight Lock
在Jdk1.6时,引入了轻量级锁。自旋锁,自适应锁。
3.5 Biased Lock
在Jdk1.6时,引入了偏向锁。
4 遗留问题
使用synchronized
来进行同步是非常方便的,JVM也在进行持续的优化,性能也可以得到满足。但是因为这一切都是JVM内部实现的,有些个别的需求它依然无法满足。
- 有些情况下效率达不到要求。
- 获取锁的状态。
- 不可中断。
- 不够灵活。
5 参考资料
《Eliminating Synchronization-Related Atomic Operations withBiased Locking and Bulk Rebiasing》 : https://www.oracle.com/technetwork/java/biasedlocking-oopsla2006-wp-149958.pdf
Synchronization and Object Locking : https://wiki.openjdk.java.net/display/HotSpot/Synchronization