在JAVA的并发编程中Synchronized是最常用的关键字之一.
它一般可以用于修饰代码块, 普通方法以及静态方法.
今天就一起看下它是如何完成锁机制的.
Synchronized代码块处理
以如下方法为例
public void test1(){
synchronized(this){
System.out.println(1);
}
}这是一个非常简单的同步逻辑,我们利用javap命令查看它对应的字节指令是什么样的.
javap -v -p -l -s MyObject.class 字节指令如下:
public void test1();
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 #8 // Field java/lang/System.out:Ljava/io/PrintStream;
7: iconst_1
8: invokevirtual #9 // Method java/io/PrintStream.println:(I)V
11: aload_1
12: monitorexit
13: goto 21
16: astore_2
17: aload_1
18: monitorexit
19: aload_2
20: athrow
21: return可以发现2个同步相关字节指令monitorenter和monitorexit
monitorenter: 第3个字节指令.
尝试获得锁, 进入同步代码块流程. 后续对象监视器小节中会详细介绍锁获得流程.
monitorexit: 第12和18个字节指令
退出同步代码块指令, 2个字节指令分别表示正常退出释放锁和异步退出释放锁.
Synchronized方法处理
以如下方法为例, 看下同步方法是如何做的.
public synchronized void test2(){
System.out.println(1);
}同样使用用javap命令查看字节指令
javap -v -p -l -s MyObject.class 指令如下:
public synchronized void test2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED方法级的同步是利用ACC_SYNCHRONIZED标识做的, 当方法调用时, 被设置ACC_SYNCHRONIZED 标识的方法被调用时, 执行线程将先尝试获取monitor, 获取成功之后才能执行方法体, 方法执行完后再释放monitor.
对象监视器(objectMonitor)
上面所提到的计数器, 是不在对象头中的, 而是在对象关联的monitor中.
JVM规范中规定, 每个对象和类都可能有一个关联的对象监视器.
我们先看下对象监视器, 它是由底层C++代码写的, 在HotSpot虚拟机中它的数据结构如下:
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;
}主要涉及到的变量有5个,
_owner: 指向持有ObjectMonitor对象的线程地址;
_WaitSet: 存放调用wait()方法, 而进入等待状态的线程的队列;
_EntryList: 这里是等待锁block状态的线程的队列;
_recursions: 锁的重入次数;
_count: 线程获取锁的次数;
在多线程执行时, 各部分对应功能如下图

1.如果monitor的所有者(_owner)为null, 则该线程进入monitor, 进入数(_recursions)设置为1, 该线程设置为monitor的所有者(_owner).
2.如果线程已经占有该monitor, 则进入monitor的进入数(_recursions)加1.
3. 如果其他线程已经占用了monitor, 则该线程进入blocked状态, 进入EntryList中, 直到monitor的进入数(_recursions)为0, 再尝试获取monitor的所有权.
4. 当线程调用wait()方法时, 会进入WaitSet中, 并释放相关锁资源.
5. 当其他线程调用notify() 或notifyAll() 方法唤醒时, 会重新尝试获得锁资源.
6. 释放锁时, monitor的进入数(_recursions)减1, 如果此时进入数(_recursions)为0, 那线程退出monitor, 不再是这个monitor的所有者. 其他被阻塞的线程就可以尝试去获取这个monitor的所有权.
对象头: markword
我们了解了monitor相关指令, 也了解了对象监视器(objectMonitor). 那两者又是怎么关联到一起的呢?
之前, 在Java 对象在内存里已经讲过了, 一个对象在内存中的存放结构是什么样的.
这里再详细说下, 与锁密切相关的对象头中的markword部分.
markword在不同的线程竞争情况下, 结构组成以及每个bit所代表的含义是不同的.
在重量级锁的情况下, 前62位指向的就是对象监视器(objectMonitor). 具体结构如下图:

锁升级
无锁
没有线程执行同步方法或代码块时的状态.
偏向锁
对象头中记录线程ID, 下次进入时直接比较线程ID, 相同则直接进入即可.
轻量级锁
在偏向锁的基础上, 有其他线程竞争时, 会升级为轻量级锁. 轻量级锁一般采用自旋锁, 自旋锁又分为固定次数自旋锁和自适应自旋锁.
当线程获取到锁时, 会将对象的markword信息复制到自己线程栈的锁记录空间(DisplacedMarkWord)中. 再次使用会用CAS的方式比较markword信息,判断锁的所有者.
重量级锁
在轻量级锁基础上, 有其他线程以自旋的方式获取锁失败, 则升级为重量级锁, 也就会用到本文介绍的对象监视器(objectMonitor).