synchronized原理
synchronized代码块是由一对monitorenter/monitorexit指令实现的,monitor对象是同步的基本实现单元。
// synchronized(object1)代码块
public void printA() {
synchronized (object1) {
try{
Thread.sleep(3000);
}catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
如上面的synchronized代码块,利用javap工具查看生成的class文件信息如下图。当线程执行到monitorenter指令时,将尝试获取线程对应monitor的所有权,即尝试获取对象的锁。
![](https://img.haomeiwen.com/i9801176/39c20bde6aa9869f.png)
- 在java6之前,Monitor的实现完全是依靠操作系统内部的互斥锁,因此需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作
- 现代的(Oracle)JDK中,JVM对此进行了大刀阔斧地改进,提供了三种不同的Monitor实现,也就是三种不同的锁:偏斜锁、轻量级锁和重量级锁。jvm会根据不同的竞争状况自动切换到合适的锁实现,这种切换就是锁的升级、降级。
synchronized是JVM内部的Intrinsic Lock(内在锁),所以偏斜锁、轻量级锁、重量级锁的代码实现,并不在核心类库部分,而是在JVM的代码中。在介绍三类锁之前,先介绍一个重要的概念:java对象头。
java对象头
synchronized用的锁是存在Java对象头里的,那么什么是Java对象头呢?
![](https://img.haomeiwen.com/i9801176/9ef1a3b79af9a701.png)
Hotspot虚拟机的对象头包含的内容如上图。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但如果对象是数组类型,则使用三个机器码。
- Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键,所以下面将重点阐述Mark Word。
- Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- 如果对象是数组类型,则用一块来记录数组长度,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小。
Mark Word用于存储对象自身的运行时数据,如HashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。。
考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下(32位虚拟机):
![](https://img.haomeiwen.com/i9801176/daea16b09246b791.png)
64位虚拟机无锁和偏向锁状态下Mark Word如下图所示:
![](https://img.haomeiwen.com/i9801176/3d16cc8e6933875b.png)
1. 偏向锁
偏向锁的适应场景是:大部分对象生命周期中最多会被一个线程锁定。使用偏向锁时,JVM会使用CAS操作,在对象头的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及整整的互斥锁,从而降低无竞争开销。偏向锁在Java 6和Java 7中是默认启用的,但是它在应用程序启动几秒钟之后才激活。
- 可以使用JVM参数来关闭延迟
-XX:-BiasedLockingStartupDelay=0
下图展示了偏向锁的获得和撤销流程
![](https://img.haomeiwen.com/i9801176/3fb6d189dd1a1f9d.png)
1.1 获取锁
(1)检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
(2)若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
(3)如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行步骤(4);
(4)通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
(5)执行同步代码块
1.2 释放锁
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM就需要撤销偏斜锁,并切换到轻量级锁实现。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。其步骤如下:
(1)暂停拥有偏向锁的线程,判断锁对象是否还处于被锁定状态;
(2)撤销偏向锁,恢复到无锁状态(01)或者轻量级锁的状态;
偏向锁状态下没有阻塞发生,并且通过减少不必要的CAS操作(与轻量级锁相比)来降低获取锁的成本。但是,由于撤销偏向锁是很重的行为,因此,当实践中需要大量使用并发操作时,往往并不需要偏斜锁。
可以使用JVM参数来关闭偏向锁,当关闭偏向锁后,将默认获取轻量级锁。
-XX:-UseBiasedLocking=false
2.轻量级锁
引入轻量级锁的主要目的是在多数没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
![](https://img.haomeiwen.com/i9801176/b722ac765b804304.png)
2.1 获取锁
(1)判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
(2)JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
(3)判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
2.2 释放锁
轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
(1)取出在获取轻量级锁保存在Displaced Mark Word中的数据;
(2)用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行步骤(3);
(3)如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。
轻量级锁情况下没有阻塞发生,但是使用了大量的CAS操作
3. 重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
三种锁优缺点对比
![](https://img.haomeiwen.com/i9801176/a1ed774eadb97af9.png)
补充:自旋锁
轻量级锁情况下,如果某个线程竞争锁失败,会尝试使用自旋来获取锁。线程不会立马阻塞,而是通过空转的方式(for(;;;))等待锁的释放。
适用场景:自旋锁可以减少线程的阻塞,这对于锁竞争不激烈,且占用锁时间非常短的代码块来说,有较大的性能提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。
如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,造成cpu的浪费。