本文主要是【JUC】——锁机制(JUC)的文章,如果有什么需要改进的地方还请大佬指出??
🎬作者简介:大家好,我是听风与他🥇
??博客首页:CSDN主页听风与他
🌄每日一句:狠狠沉淀,顶峰相见
使用synchronized,一定是和某个对象相关联的,比如说我们要向某一段代码加锁,我们需要提供一个对象作为锁本身:
public static void main(String[] args) {
synchronized (Main.class) {
//这里使用的是Main类的Class对象作为锁
}
}
变成字节码之后的指令:
其中最关键的就是monitorenter
指令了,可以看到之后也有monitorexit
与之进行匹配(注意这里有2个),monitorenter
和monitorexit
分别对应加锁和释放锁,在执行monitorenter
之前需要尝试获取锁,每个对象都有一个monitor
监视器与之对应,而这里正是去获取对象监视器的所有权,一旦monitor
所有权被某个线程持有,那么其他线程将无法获得(管程模型的一种实现)。
synchronized使用的锁就是存储在Java对象头中的,我们知道,对象是存放在堆内存中的,而每个对象内部,都有一部分空间用于存储对象头信息,而对象头信息中,则包含了Mark Word用于存放hashCode
和对象的锁信息。
在JDK6之前,synchronized一直被成为重量级锁,monitor依赖于底层操作系统的Lock实现,Java的线程是映射到操作系统的原生线程上,切换成本较高。JDK6之后,锁的实现得到了改进。我们先从最原始的重量级锁开始:
每一个对象都有一个monitor与之关联,在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的:
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 ;
}
JDK之后默认开启,它不会将处于等待状态的线程挂起,而是通过无限循环的方式,不断检测是否能够获取锁,由于单个线程占用锁的时间非常短,所以说循环次数不会太多,可能很快就能够拿到锁并运行,这就是自旋锁。当然,仅仅是在等待时间非常短的情况下,自旋锁的表现会很好,但是如果等待时间太长,由于循环是需要处理器继续运算的,所以这样只会浪费处理器资源,因此自旋锁的等待时间是有限制的,默认情况下为10次,如果失败,那么会进而采用重量级锁机制。
在JDK6之后,自旋锁得到了一次优化,自旋的次数限制不再是固定的,而是自适应变化的,比如在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行,那么这次自旋也是有可能成功的,所以会允许自旋更多次。当然,如果某个锁经常都自旋失败,那么有可能会不再采用自旋策略,而是直接使用重量级锁。
从JDK1.6开始,为了减少获得锁和释放锁带来的性能消耗,就引入了轻量级锁。
轻量级锁的目标是,在无竞争情况下,减少重量级锁产生的性能消耗(并不是为了代替重量级锁,实际上就是赌一手同一时间只有一个线程在占用资源),包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。它不像是重量级锁那样,需要向操作系统申请互斥量。它的运作机制如下:
在即将开始执行同步代码块中的内容时,会首先检查对象的Mark Word,查看锁对象是否被其他线程占用,如果没有任何线程占用,那么会在当前线程中所处的栈帧中建立一个名为锁记录(Lock Record)的空间,用于复制并存储对象目前的Mark Word信息(官方称为Displaced Mark Word)。
接着,虚拟机将使用CAS操作将对象的Mark Word更新为轻量级锁状态(数据结构变为指向Lock Record的指针,指向的是当前的栈帧)
CAS(Compare And Swap)是一种无锁算法,它并不会为对象加锁,而是在执行的时候,看看当前数据的值是不是我们预期的那样,如果是,那就正常进行替换,如果不是,那么就替换失败。比如有两个线程都需要修改变量i的值,默认为10,现在一个线程要将其修改为20,另一个要修改为30,如果他们都使用CAS算法,那么并不会加锁访问i,而是直接尝试修改i的值,但是在修改时,需要确认i是不是10,如果是,表示其他线程还没对其进行修改,如果不是,那么说明其他线程已经将其修改,此时不能完成修改任务,修改失败。在CPU中,CAS操作使用的是cmpxchg指令,能够从最底层硬件层面得到效率的提升。
如果CAS操作失败了的话,那么说明可能这时有线程已经进入这个同步代码块了,这时虚拟机会再次检查对象的Mark Word,是否指向当前线程的栈帧,如果是,说明不是其他线程,而是当前线程已经有了这个对象的锁,直接放心大胆进同步代码块即可。如果不是,那确实是被其他线程占用了。
这时,轻量级锁一开始的想法就是错的(这时有对象在竞争资源,已经赌输了),所以说只能将锁膨胀为重量级锁,按照重量级锁的操作执行(注意锁的膨胀是不可逆的)
偏向锁相比轻量级锁更纯粹,干脆就把整个同步都消除掉,不需要再进行CAS操作了。它的出现主要是得益于人们发现某些情况下某个锁频繁地被同一个线程获取,这种情况下,我们可以对轻量级锁进一步优化。
偏向锁实际上就是专门为单个线程而生的,当某个线程第一次获得锁时,如果接下来都没有其他线程获取此锁,那么持有锁的线程将不再需要进行同步操作。
可以从之前的MarkWord结构中看到,偏向锁也会通过CAS操作记录线程的ID,如果一直都是同一个线程获取此锁,那么完全没有必要在进行额外的CAS操作。当然,如果有其他线程来抢了,那么偏向锁会根据当前状态,决定是否要恢复到未锁定或是膨胀为轻量级锁。
如果我们需要使用偏向锁,可以添加-XX:+UseBiased
参数来开启。
所以,最终的锁等级为:未锁定 < 偏向锁 < 轻量级锁 < 重量级锁
值得注意的是,如果对象通过调用hashCode()
方法计算过对象的一致性哈希值,那么它是不支持偏向锁的,会直接进入到轻量级锁状态,因为Hash是需要被保存的,而偏向锁的Mark Word数据结构,无法保存Hash值;如果对象已经是偏向锁状态,再去调用hashCode()
方法,那么会直接将锁升级为重量级锁,并将哈希值存放在monitor
(有预留位置保存)中。