【多线程高并发】synchronized锁升级过程及其实现原理

问:为什么会有锁升级的过程呢?
答:在java6以前synchronized锁实现都是重量级锁的形式,效率低下,为了提升效率进行了优化,所以出现了锁升级的过程。
问:我们通常说synchronized锁是重量级锁,那么为什么叫他重量级锁?
答:因为synchronized执行效率太低。在java1.6以前每次调用synchronized加锁时都需要进行系统调用,系统调用会涉及到用户态和内核态的切换,系统调用会经过0x80中断,经过内核调用后再返回用户态。此过程比较复杂时间比较长所以通常叫synchronized为重量级锁。
误区:其实锁升级过程中涉及到的锁偏向锁,轻量级锁都是synchronized锁的具体实现所要经历的过程,他们并不是单独的锁。只是给他们这几种锁的状态起了一个名字而已。

CAS

在介绍synchronized锁升级过程之前,我们需要先了解cas的原理,为什么呢?因为cas贯穿了整个synchronized锁升级的过程。

CAS : compare and swap 或者 compare and exchange 比较交换。
当我们需要对内存中的数据进行修改操作时,为了避免多线程并发修改的情况,我们在对他进行修改操作前,先读取他原来的值E,然后进行计算得出新的的值V,在修改前去比较当前内存中的值N是否和我之前读到的E相同,如果相同,认为其他线程没有修改过内存中的值,如果不同,说明被其他线程修改了,这时,要继续循环去获取最新的值E,再进行计算和比较,直到我们预期的值和当前内存中的值相等时,再对数据执行修改操作。

CAS具体流程如下下图:
【多线程高并发】synchronized锁升级过程及其实现原理_第1张图片
他是为了实现java中的原子操作而出现的。为了保证在比较完成后赋值这两个操作的原子性,jvm内部实现cas操作时通过LOCK CMPXCHG指令锁cpu总线方式实现原子操作的。

对象头

synchronized用的锁是存在java对象头里的。

32位java对象头结构如下表所示:
在这里插入图片描述

对于64位的java对象头其余信息基本不变,只是中间有关于对象hashcode值和之后加锁信息的位数加大以外,其他基本不变。
64位虚拟机系统下java对象头在不同锁状态下的状态变化如下表所示:
【多线程高并发】synchronized锁升级过程及其实现原理_第2张图片
如上图所示:其中最后两位代表是否加锁的标志位。锁标志位如果是01的话需要根据前一位的是否为偏向锁来判断当前的锁状态,如果前一位为0则代表无锁状态,如果为1则代表有偏向锁。
后两位:00代表轻量级锁,10代表重量级锁,11代表GC垃圾回收的标记信息。

偏向锁

偏向锁产生的原因?
大多数情况下,锁不紧不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

获取偏向锁流程:
当一个线程访问同步块时,会先判断锁标志位是否为01,如果是01,则判断是否为偏向锁,如果是,会先判断当前锁对象头中是否存储了当前的线程id,如果存储了,则直接获得锁。如果对象头中指向不是当前线程id,则通过CAS尝试将自己的线程id存储进当前锁对象的对象头中来获取偏向锁。当cas尝试获取偏向锁成功后则继续执行同步代码块,否则等待安全点的到来撤销原来线程的偏向锁,撤销时需要暂停原持有偏向锁的线程,判断线程是否活动状态,如果已经退出同步代码块则唤醒新的线程开始获取偏向锁,否则开始锁竞争进行锁升级过程,升级为轻量级锁。

偏向锁获取流程如下图:
【多线程高并发】synchronized锁升级过程及其实现原理_第3张图片
在高并发下可以关闭偏向锁来提升性能,通过设置JVM参数 -XX:-UseBiasedLocking=false。

轻量级锁

当出现锁竞争时,会升级为轻量级锁。
在升级轻量级锁之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间即将对象头中用来标记锁信息相关的内容封装成一个java对象放入当前线程的栈帧中,这个对象称为LockRcord,然后线程尝试通过CAS将对象头中mark word替换为指向锁记录(lockrecord)的指针。如果成功则当前线程获取锁,如果失败则使用自旋来获取锁。自旋其实就是不断的循环进行CAS操作直到能成功替换。所以轻量级锁又叫自旋锁。

下图来源于网络
栈上分配LockRecord如下图: lockrecord中包含了对象的引用地址。
【多线程高并发】synchronized锁升级过程及其实现原理_第4张图片
对象头中markword替换锁记录指针成功之后如下图:
【多线程高并发】synchronized锁升级过程及其实现原理_第5张图片

替换成功之后将锁标志位改为00 表示获取轻量级锁成功。
lockrecord的作用:在这里实现了锁重入,每当同一个线程多次获取同一个锁时,会在当前栈帧中放入一个lockrecord,但是重入是放入的lockrecord关于锁信息的内容为null,代表锁重入。当轻量级解锁时,每解锁一次则从栈帧中弹出一个lockrecord,直到为0.
轻量级锁重入之后如下图:
【多线程高并发】synchronized锁升级过程及其实现原理_第6张图片

当通过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加锁如下图:
【多线程高并发】synchronized锁升级过程及其实现原理_第7张图片
重量级锁在进行锁重入的时候每获取到锁一次会对monitor对象中的计数器+1,等锁退出时则会相应的-1,直到减到0为止,锁完全退出。

几种锁状态优缺点对比

【多线程高并发】synchronized锁升级过程及其实现原理_第8张图片

总结

综上,我们发现偏向锁,轻量级锁(又称自旋锁或无锁),重量级锁都是synchronized锁锁实现中锁经历的几种不同的状态。
三种锁状态的场景总结:

  • 只有一个线程进入临界区 -------偏向锁
  • 多个线程交替进入临界区--------轻量级锁
  • 多个线程同时进入临界区-------重量级锁

你可能感兴趣的:(多线程)