本文链接:https://blog.csdn.net/wangyy130/article/details/106495180
问:为什么会有锁升级的过程呢
答:在java6以前synchronized锁实现都是重量级锁的形式,效率低下,为了提升效率进行了优化,所以出现了锁升级的过程。
问:我们通常说synchronized锁是重量级锁,那么为什么叫他重量级锁?
答:因为synchronized执行效率太低。在java1.6以前每次调用synchronized加锁时都需要进行系统调用,系统调用会涉及到用户态和内核态的切换,系统调用会经过0x80中断,经过内核调用后再返回用户态。此过程比较复杂时间比较长所以通常叫synchronized为重量级锁。
误区:其实锁升级过程中涉及到的锁偏向锁,轻量级锁都是synchronized锁的具体实现所要经历的过程,他们并不是单独的锁。只是给他们这几种锁的状态起了一个名字而已。
synchronized锁有四种状态,无锁,偏向锁,轻量级锁,重量级锁,这几个状态会随着竞争状态逐渐升级,锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态
在介绍synchronized锁升级过程之前,我们需要先了解cas的原理,为什么呢?因为cas贯穿了整个synchronized锁升级的过程。
CAS : compare and swap 或者 compare and exchange 比较交换。
当我们需要对内存中的数据进行修改操作时,为了避免多线程并发修改的情况,我们在对他进行修改操作前,先读取他原来的值E,然后进行计算得出新的的值V,在修改前去比较当前内存中的值N是否和我之前读到的E相同,如果相同,认为其他线程没有修改过内存中的值,如果不同,说明被其他线程修改了,这时,要继续循环去获取最新的值E,再进行计算和比较,直到我们预期的值和当前内存中的值相等时,再对数据执行修改操作。
CAS具体流程如下下图:
他是为了实现java中的原子操作而出现的。为了保证在比较完成后赋值这两个操作的原子性,jvm内部实现cas操作时通过LOCK CMPXCHG指令锁cpu总线方式实现原子操作的。
synchronized用的锁是存在java对象头里的。
32位java对象头结构如下表所示:
对于64位的java对象头其余信息基本不变,只是中间有关于对象hashcode值和之后加锁信息的位数加大以外,其他基本不变。
64位虚拟机系统下java对象头在不同锁状态下的状态变化如下表所示:
如上图所示:其中最后两位代表是否加锁的标志位。锁标志位如果是01的话需要根据前一位的是否为偏向锁来判断当前的锁状态,如果前一位为0则代表无锁状态,如果为1则代表有偏向锁。
后两位:00代表轻量级锁,10代表重量级锁,11代表GC垃圾回收的标记信息。
偏向锁产生的原因?
大多数情况下,锁不紧不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
获取偏向锁流程:
当一个线程访问同步块时,会先判断锁标志位是否为01,如果是01,则判断是否为偏向锁,如果是,会先判断当前锁对象头中是否存储了当前的线程id,如果存储了,则直接获得锁。如果对象头中指向不是当前线程id,则通过CAS尝试将自己的线程id存储进当前锁对象的对象头中来获取偏向锁。当cas尝试获取偏向锁成功后则继续执行同步代码块,否则等待安全点的到来撤销原来线程的偏向锁,撤销时需要暂停原持有偏向锁的线程,判断线程是否活动状态,如果已经退出同步代码块则唤醒新的线程开始获取偏向锁,否则开始锁竞争进行锁升级过程,升级为轻量级锁。
另一个博主的解释:
当线程1访问代码块并获取锁对象时,会在java对象头和栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
偏向锁获取流程如下图:
在高并发下可以关闭偏向锁来提升性能,通过设置JVM参数 -XX:-UseBiasedLocking=false。
当出现锁竞争时,会升级为轻量级锁。
在升级轻量级锁之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间即将对象头中用来标记锁信息相关的内容封装成一个java对象放入当前线程的栈帧中,这个对象称为LockRcord,然后线程尝试通过CAS将对象头中mark word替换为指向锁记录(lockrecord)的指针。如果成功则当前线程获取锁,如果失败则使用自旋来获取锁。自旋其实就是不断的循环进行CAS操作直到能成功替换。所以轻量级锁又叫自旋锁。
另一个博主的解释:
线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址;
如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。 自旋锁简单来说就是让线程2在循环中不断CAS
但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。
下图来源于网络
栈上分配LockRecord如下图: lockrecord中包含了对象的引用地址。
对象头中markword替换锁记录指针成功之后如下图:
替换成功之后将锁标志位改为00 表示获取轻量级锁成功。
lockrecord的作用:在这里实现了锁重入,每当同一个线程多次获取同一个锁时,会在当前栈帧中放入一个lockrecord,但是重入是放入的lockrecord关于锁信息的内容为null,代表锁重入。当轻量级解锁时,每解锁一次则从栈帧中弹出一个lockrecord,直到为0.
轻量级锁重入之后如下图:
当通过CAS自旋获取轻量级锁达到一定次数时,JVM会发生锁膨胀升级为重量级锁。
原因:不断的自旋在高并发的下会消耗大量的cpu资源,所以jvm为了节省cpu资源,进行了锁升级。将等待获取锁的线程都放入一个等待队列中来节省cpu资源。
在重量级锁中将LockRecord对象替换为了monitor对象的实现。主要通过monitorenter和monitorexit两个指令来实现。需要经过系统调用,在并发低的情况下效率会低。
通过openJDK可以查看ObjectMonitor对象的结构:http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/9758d9f36299/src/share/vm/runtime/objectMonitor.hpp
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //拥有当前对象的线程
_WaitSet = NULL; //阻塞队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //有资格成为候选资源的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
关于Monitor详解:https://blog.csdn.net/qq_35044419/article/details/116756409
综上,我们发现偏向锁,轻量级锁(又称自旋锁或无锁),重量级锁都是synchronized锁锁实现中锁经历的几种不同的状态。
三种锁状态的场景总结: