Synchronized 锁机制的实现原理
Synchronized是Java种用于进行同步的关键字,synchronized的底层使用的是锁机制实现的同步。在Java中的每一个对象都可以作为锁。
Java中synchronized的两个特性:
互斥性:即在同一时间内只允许同一个县城持有某一个对象锁,通过这种特性来实现多个线程中的协调机制,这样在同一时间内只有一个线程对同步的代码进行访问,互斥性往往也被称为原子性。
可见性:必须确保在获取锁的时候,线程内共享变量的值和主存一致,并且也必须保证在锁在被释放前,对共享变量所做的修改,对于随后获取锁的另一个线程是可见的(即在获取锁时应该获得的是最新的共享变量的值),否则另一个线程可能在本地缓存的某一个副本上继续操作从而导致结果不一致。
synchronized锁具体的三种形式:
- 对于普通同步方法,锁对象是当前实例对象,进入同步代码块前需要获得当前对象的锁。
- 对于静态同步方法,锁的是当前类的对象,在Java中每一个类都有一个Class对象。
- 对于同步方法块,锁的是synchronized圆括号内的对象,这里的对象可以是一个普通的对象,也可以是一个Class对象,如果是Class对象的话,也就是所谓的类锁了,而类锁是通过类的Class对象实现的。
synchronized的原理
JVM基于进入和推出Monitor对象来实现同步方法和同步代码块,但两者的实现细节不同。
- 同步代码块是使用
monitorenter
和monitorexit
指令实现的。 - synchronized修饰的方法并没有
monitorenter
和monitorexit
指令,而取代之的是ACC_SYNCHRONIZED标识,该标志指明了该方法是一个同步方法,从而执行相应的同步调用。
monitorenter
指令实现编译后茶后到同步代码块开始的位置,而monitorexit
是插入在方法结束出和异常处,JVM要保证每个monitorenter
必须要有一个与之相对应的monitorexit
。**任何一个对象都可以和一个monitor相关联,且当一个执行monitorenter
指令的时候,会尝试获取synchronized圆括号中对象中相关联的monitor的所有权,即尝试获取这个对象的锁(这里的是重量级锁,而没有后面提到的偏向锁和轻量级锁)。
下面来看一个具体的例子:
public class SynchronizedTest {
public void readFile() throws IOException {
synchronized(this) {
System.out.println("同步代码块");
}
}
}
经过javap反编译后,结果如下:
[图片上传失败...(image-96303b-1550504786733)]
可以看出synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
下面的是同步方法:
public class SynchronizedTest {
public synchronized void readFile() throws IOException {
System.out.println("同步代码块");
}
}
反编译后如下图所示:
Java对象头
在Hotspot虚拟机中,对象头主要包含两部分数据:
- Mark Word(标记字段)
- Klass Pointer(类型指针)
MarkWord: 默认存储对象的HashCode,GC分代和锁标志位信息。这些信息都与对象自身定义无关的数据,所以Mark Word被设计成非固定数据结构,以便在极小的空间存储尽可能多的数据。(Mark Word的大小在32位虚拟机中占32个字节,在64位的虚拟机中占64个字节)。也就是说在运行期间Mark Word里面存储的数据会随着锁标志的变化而变化。
Klass Point: 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。同样的它的大小在32位虚拟机中占32个字节,在64位的虚拟机中占64个字节。
对于不同的对象头它们的总结如下表:
长度 | 内容 | 说明 |
---|---|---|
32/64bit | MarkWord | 存储对象的hashCode,GC分代和锁信息 |
32/64bit | Klass Point | 存储到类元数据的指针 |
32/64bit | Array Length | 这个只针对数组对象而言,存储数组的长度 |
Java对象头中的MarkWord里面的存储对象如下表:
锁状态 | 25bit | 4bit | 1bit是否偏向锁 | 2bit锁标志 |
---|---|---|---|---|
无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
在运行期间,Mark Word里存储的数据会随着锁标志的变化而变化,它的变化如下表所示:
在64位的虚拟下,Mark Word是64bit的存储结构,其存储结构如下表:
锁状态 | 25bit | 31bit | 1bit | 4bit | 1bit | 2bit |
---|---|---|---|---|---|---|
cms_free | 分代年龄 | 偏向锁 | 锁标志位 | |||
无锁 | unused | hashCode | 0 | 01 | ||
偏向锁 | ThreadId(54bit) | Epoch(2bit) | 1 | 01 |
对象头的最后两位存储了锁的标志,01是初始状态表示无锁,其对象头里存储的是对象的哈希吗,随着锁级别的不同,对象头中存储的内容也会不同。偏向锁存储的当前占用此对象的线程ID;而轻量级锁则是存储指向线程栈中锁记录的指针。
Monitor监视器锁
其中轻量级锁和偏向锁是Java6对synchronized锁进行优化后增加的,我们稍后会进行分析。这里我们主要分析重量级锁,也就是通常所说的synchronized对象锁,锁标识为10,其中指针指向monitor对象(也称之为管程或者监视器锁)的起始地址。每个对象都存在一个monitor与之关联,对象与其monitor之间也存在着多种实现方式,如monitor可以与对象一起创建或者销毁或当前线程试图获取锁时自动生成,但一个monitor被某线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是有ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet和_EntryList,用来保存ObjectWaiter对象列表,每个等待锁的线程都会被封装成ObjectWaiter对象,_owner指向指向持有ObjectMonitor对象的线程,当多个线程同时访问同一同步代码块或者同步方法时,首先会进入_EntryList队列,当线程获取到monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示:
所以,monitor对象存在于每一个Java对象的对象头(存储指针的指向),synchronized锁便是通过这种方式获取的,也是为什么Java中任意对象都可以作为锁的原因,同时也是notify/notifyAll/wait方法等存在于顶级对象Object中的原因。