最近在准备JAVA面试的时候遇到了一个问题,是关于java中synchronized的使用,基本上每次必考。但每次再面试官的深挖下回答上总会出现纰漏。
所以借着下面这个问题,来好好总结一下synchronized的相关内容。
synchronized 是否是可重入的?
在维基百科中,可重入性互斥(Reentrant mutex)是这么定义的:
In computer science, the reentrant mutex (recursive mutex, recursive lock) is particular type of mutual exclusion (mutex) device that may be locked multiple times by the same process/thread, without causing a deadlock.
可重入互斥体,又叫递归互斥、递归锁,是一种典型的互斥体,可以被同一个进程、线程重复开锁多次,且不会导致死锁。
那么synchronized是否符合这个标准呢?可以从下面的例子看出:
public class SynchronizedTest implements Runnable{
public void func(int seed) {
synchronized(this) {
if (seed <= 20) {
System.out.println(Thread.currentThread().getName() + ":" +seed);
func(++seed);
}
return;
}
}
@Override
public void run() {
// TODO Auto-generated method stub
try {
func(1);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
Thread thread1 = new Thread(test, "SyncThread1");
Thread thread2 = new Thread(test, "SyncThread2");
thread1.start();
thread2.start();
}
上面的代码由两个线程组成,并使 synchronized 的方法递归调用自己本身,得到如下结果。
SyncThread2:1
SyncThread2:2
SyncThread2:3
SyncThread2:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
SyncThread2:10
SyncThread2:11
SyncThread2:12
SyncThread2:13
SyncThread2:14
SyncThread2:15
SyncThread2:16
SyncThread2:17
SyncThread2:18
SyncThread2:19
SyncThread2:20
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread1:5
SyncThread1:6
SyncThread1:7
SyncThread1:8
SyncThread1:9
SyncThread1:10
SyncThread1:11
SyncThread1:12
SyncThread1:13
SyncThread1:14
SyncThread1:15
SyncThread1:16
SyncThread1:17
SyncThread1:18
SyncThread1:19
SyncThread1:20
从结果上来看,synchronized关键字显然是可重入的。
synchronized 的实现过程
下面这段是将上述代码的synchronized代码段的class字节码:
3 monitorenter
4 iload_1 [seed]
5 bipush 20
7 if_icmpgt 52
10 getstatic java.lang.System.out : java.io.PrintStream [18]
13 new java.lang.StringBuilder [24]
16 dup
17 invokestatic java.lang.Thread.currentThread() : java.lang.Thread [26]
20 invokevirtual java.lang.Thread.getName() : java.lang.String [32]
23 invokestatic java.lang.String.valueOf(java.lang.Object) : java.lang.String [36]
26 invokespecial java.lang.StringBuilder(java.lang.String) [42]
29 ldc <String ":"> [45]
31 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [47]
34 iload_1 [seed]
35 invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [51]
38 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [54]
41 invokevirtual java.io.PrintStream.println(java.lang.String) : void [57]
44 aload_0 [this]
45 iinc 1 1 [seed]
48 iload_1 [seed]
49 invokevirtual studyjava.SynchronizedTest.func(int) : void [62]
52 aload_2
53 monitorexit
可以看到生成的字节码中包含 monitorenter
和monitorexit
两条指令。
synchronized关键字基于上述两个指令实现了锁的获取和释放过程。
在hotspot 虚拟机中,monitorenter
时会进入到InterpreterRuntime.cpp
的InterpreterRuntime::monitorenter
函数。
图片来源于,简书占小狼。
1、JavaThread thread指向java中的当前线程;
2、BasicObjectLock类型的elem对象包含一个BasicLock类型_lock对象和一个指向Object对象的指针_obj;
class BasicObjectLock {
BasicLock _lock;
// object holds the lock;
oop _obj;
}
3、BasicLock类型_lock对象主要用来保存_obj指向Object对象的对象头数据;
class BasicLock {
volatile markOop _displaced_header;
}
4、UseBiasedLocking标识虚拟机是否开启偏向锁功能,如果开启则执行fast_enter逻辑,否则执行slow_enter;
可重入性从偏向锁开始
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。上面说过,轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
1、偏向锁获取过程:
(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01——确认为可偏向状态。
(2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
(5)执行同步代码。
2、偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
从这里我们可以看出,偏向锁会检查线程是否是当前锁的持有者。这允许了可重入性的发生。
升级到轻量级锁
轻量级锁同样会检查当前线程,保证可重入性。
1、轻量级锁的加锁过程
(1)在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
(2)拷贝对象头中的Mark Word复制到锁记录中。
(3)拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(3),否则执行步骤(4)。
(4)如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图2.2所示。
(5)如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
2、轻量级锁的解锁过程:
(1)通过CAS操作尝试把线程中复制的Displaced Mark Word对象替换当前的Mark Word。
(2)如果替换成功,整个同步过程就完成了。
(3)如果替换失败,说明有其他线程尝试过获取该锁(此时锁已膨胀),那就要在释放锁的同时,唤醒被挂起的线程。
最后轮到了重量锁
重量锁就是概念上的我们提到的互斥锁,也就是当存在锁竞争时,所有没有得到锁的线程将会被挂起。
总结
本篇文章是我从可重入性开始分析synchronized。
主要参考了:
https://www.jianshu.com/p/c5058b6fe8e5
https://www.jianshu.com/p/c79c5e02ebe6
http://www.cnblogs.com/paddix/p/5405678.html
从中我们得知,synchronized通过在每次竞争锁时检查线程是否是当前锁的持有者来实现可重入性。
值得注意的是,偏向锁、轻量级锁本身是对锁的一种优化措施。从偏向锁到轻量级锁再到重量级锁在hotspot虚拟机源码中是一个相当连续的过程,而不是泾渭分明的离散状态。