一、概述
java Synchronized
锁是Java提供的,底层依赖JVM实现的单机锁
。可用于对实例方法
、静态方法
以及代码块
进行加锁,用于保护多线程环境下对共享资源访问的安全性。
二、底层原理分析
java Synchronized
锁依赖于Java对象的内存布局,因此,需要了解Java对象的内存布局。
详情可见这篇文章:深入解析Java对象创建逻辑、内存布局以及访问机制
2.1 Monitor对象
每个对象自从创建起,都会关联一个锁对象
,即Monitor对象【管程\监视器】。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针
。该对象在Java虚拟机中,由C++实现的,具体表现为:
java
ObjectMonitor,具体的数据结构为:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; // _owner指向持有ObjectMonitor对象的线程
_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
具体字段解释:
- _owner: 指向拥有ObjectMonitor对象的线程
- _WaitSet: 存放处于wait状态的的线程队列
- _EntryList: 存放处于等待锁block状态的线程队列
- _recursions:锁的重入次数
- _count:用来记录该线程获取锁的次数
工作原理:
- 当多个线程同时访问一段同步代码时。首先假设线程A会进入EntryList队列中,会判断owner是否为空,主要通过
CAS操作
(比较和交换,比较新值和旧值的不同)。如果owner为null,直接把其赋值,指向自己owner=self,同时把可重入次数recursions=1,count+1获取锁成功。如果self=cur,说明是当前线程,锁重入了,recursions++即可。线程A进入owner区域,然后执行同步方法块; - 若线程B来获取锁。首先会放入EntryList队列中。然后去判断锁是否被占用,此时线程A正在使用该锁,那么会一直放在EntryList队列中,直到线程A释放锁,所有EntryList队列中竞争锁是非公平的;
- 若持有monitor的线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒;
- 线程从WaitSet集合中被唤醒notifyall后,会放入到EntryList队列中,参与锁的竞争
2.2 锁分类以及升级流程
2.2.1 偏向锁
背景:
在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。轻量级锁需要多次CAS操作,偏向锁来减少CAS的操作次数。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了降低获取所得代价,引入偏向锁。
大致流程:
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。
获取锁、释放锁以及锁升级的流程:
- 获取锁:当线程1访问代码块并获取锁对象后,会在对象头和栈帧中记录偏向锁的threadID。以后,线程1再次获取锁时,直接比较当前threadID和对象头中的threadID是否一致;如果一致,不需要CAS加锁、解锁;
- 释放锁:采用只有线程竞争时才会释放锁的机制【没有竞争时,解锁后线程ID仍会存在对象头中】。偏向锁的撤销需要等待全局安全点(在这个时间点上没有正在执行的字节码)。首先暂停拥有偏向锁的线程,(1)判断拥有偏向锁的线程是否存活;若没有存活,锁对象状态被重置为无锁状态;若存活,(2)查找线程1的栈帧信息,是否需要继续持有这个锁。若需要,等待全局安全点,在安全点暂停持有偏向锁的线程1,撤销偏向锁,升级为轻量级锁(锁的升级);若不需要,将锁对象的状态设为无锁状态,重现偏向新的线程。
2.2.2 轻量级锁
背景:
在没有多线程竞争的前提下,减少传统的重量级锁
使用操作系统互斥量
产生性能消耗
。
使用场景:
如果一个对象虽然有多线程要加锁,但加锁时间是错开
的(也就是没有竞争),那么可以使用轻量级锁来优化。
使用轻量级锁的时机:
当关闭偏向锁或者多个线程竞争偏向锁导致锁升级为轻量级锁,则会尝试获取轻量级锁,获取锁的流程如下:
- JVM在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中:
- 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录。
- 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁:
- 如果 cas 失败,有两种情况:
● 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
● 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。
【解锁流程】如下所示: - 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一;
- 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用CAS将 Mark Word 的值恢复给对象头:
● 成功,则解锁成功
● 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
【锁膨胀流程 】:
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。 - 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁;
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
● 为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
● 然后自己进入 Monitor 的 EntryList BLOCKED。
- 当 Thread-0 退出同步块解锁时,使用cas将Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
2.2.3 重量级锁
缺点:
- ObjectMonitor源码时,会发现一些内核函数,对应的线程为park()和upark(),该操作涉及到用户态和内核态,从用户态切换到内核态是非常消耗资源的
- 用户态:程序的运行空间进入到用户运行状态;
- 内核态:涉及到IO操作
2.2.3.1 自旋优化-自旋锁
出现原因:
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力
解决方案:
让线程等待一段时间,不会立即被挂起,看持有锁的线程是否会很快释放锁
优缺点:
- **优点:**可以避免线程切换的开销;但占用了处理器CPU的时间,如果持有锁的线程很快就释放了锁,自旋效率是很高的;否则,会消耗掉大量的资源
- **缺点:**对自旋次数进行了限制,默认为10次。如果次数到限制,刚刚退出,锁被释放(多等一两次就可以获得锁)是非常遗憾的
2.2.3.2 自旋优化-自适应自旋锁
即自旋的次数不在固定,取决于前一次在同一锁上的自旋时间以及锁的拥有者的状态来决定。如果自旋成功了,下次自旋的次数就会增加,即jvm会认为自旋获得锁的成功率会很高;反之,对于某个锁,很少有能自旋成功的,在以后自旋时,会相应减少自选的次数。
锁消除:
- 如果不存在竞争,为什么还需要加锁?可以将锁消除。
- 锁消除依据:逃逸分析的数据支持,如果变量没有逃逸出方法,又因为栈是线程私有的,所以不会存在竞争情况,可以放心清除锁,节省毫无意义的请求锁的时间
锁粗化:
出现背景:
如果发生同一对象进行一系列的加锁解锁操作,会导致不必要的性能消耗解决方法:
锁粗化,即将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,降低加锁解锁的次数。
2.2.4 各种锁的对比
2.2.5 synchronized 特性
- 可重入性:
● Synchronized锁对应的时候有一个计数器,记录下线程获取锁的次数,每次获取锁,计数器加1,每次释放锁,计数器减1,直到计数器为0,锁全部释放。
● 好处:可以避免一些死锁的情况 - 不可中断性:
● 一个线程获取锁之后,另一个线程处于阻塞或者等待状态,前一个线程不释放锁,后一个线程会一直阻塞或者等待,不可以被中断
2.2.6 Synchronized针对同步代码块和同步方法的底层原理
- 同步代码块:
● 对象头会关联到一个monitor对象。进入一个方法的时候执行monitorEnter(同步代码块的开始位置),获取当前对象的一个所有权owner,monitor数值加1,当前这个线程就是monitor的owner,退出的时候对应monitorexit(插入到方法结束处和异常处)。monitorenter和monitorexit是一对,缺一不可。
● 如果已经拥有owner,再次获得锁(可重入),计数器加1,执行monitorexit时,计数器减1;
● 互斥性体现在:是否能够获得monitor的所有权 - 同步方法
● 与同步代码块类似,多了一个标识位ACC_SYNCHRONIZED,一旦执行方法的时候,就会先判断是否存在 标志位,然后ACC_SYNCHRONIZED会隐式的调用monitorenter和monitorexit。归根到底,还是monitor的争夺。