- [1. Synchronized底层原理](#1-synchronized底层原理)
- [1.1. Java对象头与monitor](#11-java对象头与monitor)
- [1.1.1. JVM内存对象](#111-jvm内存对象)
- [1.1.2. 对象头详解](#112-对象头详解)
- [1.1.2.1. Mark Word](#1121-mark-word)
- [1.1.2.2. class pointer](#1122-class-pointer)
- [1.1.2.3. array length](#1123-array-length)
- [1.1.3. monitor对象](#113-monitor对象)
- [1.2. Synchronized底层实现](#12-synchronized底层实现)
- [1.2.1. 同步代码块](#121-同步代码块)
- [1.2.2. 同步方法](#122-同步方法)
**总结:**
Synchronized的底层原理:
**Java对象头的MarkWord中除了存储锁状态标记外,还存有ptr_to_heavyweight_monitor(也称为管程或监视器锁)的起始地址,每个对象都存在着一个monitor与之关联。**
**monitor运行的机制过程如下:(_WaitSet队列和 _EntryList队列)**
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-55.png)
* 想要获取monitor的线程,首先会进入_EntryList队列。
* 当某个线程获取到对象的monitor后,进入Owner区域,设置为当前线程,同时计数器count加1。
* 如果线程调用了wait()方法,则会进入WaitSet队列。它会释放monitor锁,即将owner赋值为null,count自减1,进入WaitSet队列阻塞等待。
* 如果其他线程调用 notify() / notifyAll(),会唤醒WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入Owner区域。
* 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。
Synchronized方法同步:依靠的是方法修饰符上的ACC_Synchronized实现。
Synchronized代码块同步:使用monitorenter和monitorexit指令实现。
# 1. Synchronized底层原理
## 1.1. Java对象头与monitor
Java对象头是实现synchronized的锁对象的基础。
### 1.1.1. JVM内存对象
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充,如下所示:
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-65.png)
* 对象头:包含Mark Word、class pointer、array length共3部分。
* **第一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁标识状态、线程持有的锁、偏向线程ID等。** 这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特,官方称它为“Mark Word”。这部分是实现轻量级锁和偏向锁的关键。
* 另外一部分指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。
* 还会有一个额外的部分用于存储数组长度。
* 实例数据:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
* 对齐填充:JVM要求对象起始地址必须是8字节的整数倍(8字节对齐)。填充数据不是必须存在的,仅仅是为了字节对齐。
JVM中对象头的方式有以下两种(以32位JVM为例)
* 普通对象:
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-60.png)
* 数组对象:
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-61.png)
### 1.1.2. 对象头详解
#### 1.1.2.1. Mark Word
**由于对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到Java虚拟机的空间使用效率,** **Mark Word被设计成一个非固定的动态数据结构,** 以便在极小的空间内存储尽量多的信息。它会根据对象的状态复用自己的存储空间。
这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。
为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:
64位下的标记字与32位的相似:
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-41.png)
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-62.png)
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-64.png)
下面两张图是32位JVM和64位JVM中“Mark Word”所记录的信息
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-67.png)
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-68.png)
其中各部分的含义如下:(用对象头中markword最低的三位代表锁状态,其中1位是偏向锁位,两位是普通锁位。)
* lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-63.png)
* bias_lock:对象是否启动偏向锁标记,只占1个二进制位。为1时表示对象启动偏向锁,为0时表示对象没有偏向锁。
* age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
* identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
* thread:持有偏向锁的线程ID。
* epoch:偏向时间戳。
* ptr_to_lock_record:指向栈中锁记录的指针。
* **ptr_to_heavyweight_monitor:指向monitor对象(也称为管程或监视器锁)的起始地址,每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor对象可以与对象一起创建销毁或当前线程试图获取对象锁时自动生,但当一个monitor被某个线程持有后,它便处于锁定状态。**
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-56.png)
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-76.png)
为什么锁信息存放在对象头里?
因为在Java中任意对象都可以用作锁,因此必定要有一个映射关系,存储该对象以及其对应的锁信息(比如当前哪个线程持有锁,哪些线程在等待)。一种很直观的方法是,用一个全局map,来存储这个映射关系,但这样会有一些问题:需要对map做线程安全保障,不同的Synchronized之间会相互影响,性能差;另外当同步对象较多时,该map可能会占用比较多的内存。
所以最好的办法是将这个映射关系存储在对象头中,因为对象头本身也有一些hashcode、GC相关的数据,所以如果能将锁信息与这些信息共存在对象头中就好了。
也就是说,如果用一个全局 map 来存对象的锁信息,还需要对该 map 做线程安全处理,不同的锁之间会有影响。所以直接存到对象头。
#### 1.1.2.2. class pointer
这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:
* 每个Class的属性指针(即静态变量)
* 每个对象的属性指针(即对象变量)
* 普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。
#### 1.1.2.3. array length
如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。
### 1.1.3. monitor对象
每个对象实例都会有个Monitor对象,Monitor对象和Java对象一同创建并消毁,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,在虚拟机的ObjectMonitor.hpp文件中。ObjectMonitor的成员变量如下( Hospot 1.8) :
```C
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 线程重入次数
_object = NULL; // 存储Monitor对象
_owner = NULL; // 持有当前线程的owner
_WaitSet = NULL; // wait状态的线程列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 单向列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁状态block状态的线程列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
```
ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象)。
**monitor运行的机制过程如下:(_WaitSet队列和 _EntryList队列)**
![image](https://gitee.com/wt1814/pic-host/raw/master/images/java/concurrent/multi-55.png)
* 想要获取monitor的线程,首先会进入_EntryList队列。
* 当某个线程获取到对象的monitor后,进入Owner区域,设置为当前线程,同时计数器count加1。
* 如果线程调用了wait()方法,则会进入WaitSet队列。它会释放monitor锁,即将owner赋值为null,count自减1,进入WaitSet队列阻塞等待。
* 如果其他线程调用 notify() / notifyAll(),会唤醒WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入Owner区域。
* 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。
因此,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。
下面再看一下加锁的代码:
```text
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
//获取当前线程指针
Thread * const Self = THREAD ;
void * cur ;
//尝试通过CAS将OjectMonitor的_owner设置为当前线程;
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
//尝试失败
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
//如果cur等于当前线程,说明当前线程已经持有锁,即为锁重入,_recursions自增,并获得锁。
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
//第一次设置_owner成功,锁的重入次数_recursions设置为1,_owner设置为当前线程,
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged "Thread *".
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
//代码省略。。。。。
// TODO-FIXME: change the following for(;;) loop to straight-line code.
//如果竞争是失败的,会进入下面中的无线循环,反复调用EnterI方法,自旋尝试获取锁
for (;;) {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don't want to enter
// the monitor while suspended because that would surprise the
// thread that suspended us.
//
_recursions = 0 ;
_succ = NULL ;
exit (false, Self) ;
jt->java_suspend_self();
}
//代码省略。。。。。。。。。。。。。
```
下面再看一下释放锁的代码:
```text
void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * Self = THREAD ;
if (THREAD != _owner) {
if (THREAD->is_lock_owned((address) _owner)) {
// Transmute _owner from a BasicLock pointer to a Thread address.
// We don't need to hold _mutex for this transition.
// Non-null to Non-null is safe as long as all readers can
// tolerate either flavor.
assert (_recursions == 0, "invariant") ;
_owner = THREAD ;
_recursions = 0 ;
OwnerIsThread = 1 ;
} else {
// NOTE: we need to handle unbalanced monitor enter/exit
// in native code by throwing an exception.
// TODO: Throw an IllegalMonitorStateException ?
TEVENT (Exit - Throw IMSX) ;
assert(false, "Non-balanced monitor enter/exit!");
if (false) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
return;
}
}
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}
//代码省略。。。。。。。。。。。。。
}
```
## 1.2. Synchronized底层实现
```java
public class SyncDemo {
public Synchronized void play() {}
public void learn() {
Synchronized(this) {
}
}
}
```
利用javap工具查看生成的class文件信息分析Synchronized,下面是部分信息:
查看字节码工具:
Show Uytecode With jclasslib
Show Bytccodc
```java
public com.zzw.juc.sync.SyncDemo();
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 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zzw/juc/sync/SyncDemo;
public Synchronized void play();
descriptor: ()V
flags: ACC_PUBLIC, ACC_Synchronized
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/zzw/juc/sync/SyncDemo;
public void learn();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
```
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者实现细节不同。
* **方法同步:依靠的是方法修饰符上的ACC_Synchronized实现。**
* **代码块同步:使用monitorenter和monitorexit指令实现。**
### 1.2.1. 同步代码块
monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。
两条指令的作用:
* monitorenter:
每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获取该monitor。
线程执行monitorenter指令时尝试获取monitor的所有权,当monitor被占用时就会处于锁定状态。过程如下:
1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1,所以Synchronized关键字实现的锁是可重入的锁。
3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
* monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,当前线程释放monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
**同步代码块中会出现两次的monitorexit。** 这是因为一个线程对一个对象上锁了,后续就一定要解锁,第二个monitorexit是为了保证在线程异常时,也能正常解锁,避免造成死锁。
总结:Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
### 1.2.2. 同步方法
当JVM执行引擎执行某一个方法时,其会从方法区中获取该方法的access_flags,检查其是否有ACC_SYNCRHONIZED标识符,若是有该标识符,则说明当前方法是同步方法,需要先获取当前对象的monitor,再来执行方法,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
一键复制
编辑
Web IDE
原始数据
按行查看
历史