synchronized底层原理与源码解析

synchronized底层

每个对象有一个监视器锁(monitor),当monitor被占用时处于锁定状态

访问监视器锁的方式

线程执行monitor enter指令时尝试获取monitor的所有权:

1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数+1.

3、如果其他线程已经占用monitor,该线程进入阻塞状态,直到monitor的进入数为0,再尝试获取monitor的所有权

线程执行monitor exit指令,退出

monitor的进入数 -1,如果 -1后进入数为 0,线程退出monitor;其他被monitor阻塞的线程尝试获取 monitor

 

当多个线程一起访问某个对象监视器的时候,对象监视器会将这些请求存储在不同的容器中

Lock Record 锁记录的数据机构,里面存储对象的mark word

  >Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中【可能资源还不够】

  >Entry List:Contention List中那些有资格成为候选竞争的线程被移动到Entry List中;【获取了足够资源】

  >Wait Set:调用wait方法被阻塞的线程被放置在这里,调用notify(),取出队头cas到Entry List

  >OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck线程;竞争切换--非公平性

  >Owner:当前已经获取到锁资源的线程被称为Owner

  > !Owner:当前释放锁的线程

waitingQueue

新请求锁的线程将首先被加入到ConetentionList中,当某个拥有锁的线程(Owner状态)调用unlock之后,如果发现 EntryList为空则从ContentionList中移动线程到EntryList,下面说明下ContentionList和EntryList 的实现方式:

ContentionList 虚拟队列

ContentionList并不是一个真正的Queue,而只是一个虚拟队列,原因在于ContentionList是由Node及其next指针逻辑构成,并不存在一个Queue的数据结构。ContentionList是一个后进先出(LIFO)的队列【栈,线程死亡也是从头节点出列,避免与owner争用尾节点】,每次新加入Node时都会在队头进行, 通过CAS改变第一个节点的的指针为新增节点,同时设置新增节点的next指向后续节点,而取得操作则发生在队尾。显然,该结构其实是个Lock- Free【无锁】队列。

因为只有Owner线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了CAS的ABA问题

EntryList

  1. EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对 ContentionList队尾的争用,而建立EntryList
  2. Owner线程在unlock时会指定EntryList中的某个线程(一般为Head)变为Ready(OnDeck)线程
    1. EntryList有多个线程,因为OnDeck线程并不一定能抢到锁
    2. 从ContentionList中迁移线程到 EntryList
  3. “竞争切换-非公平性”:Owner线程并不是把锁传递给 OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。
    1. 虽然牺牲了一定的公平性,但极大的提高了整体吞吐量;与进入ContentionList之前自旋的线程竞争
  4. OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中的队头,在EntryList中的位置不发生变化
  5. 如果Owner线程被wait方法阻塞,则转移到WaitSet阻塞队列;如果在某个时刻被notify/notifyAll唤醒,出队头, 则再次转移到EntryList

 

ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,处于OS内核态挂起

JVM1.6 偏向锁的作用:避免CAS

无竞争下锁存在什么问题:在JVM1.6中引入了偏向锁,偏向锁主要解决无竞争下的锁性能问题

  1. 现在几乎所有的锁都是可重入的,也即已经获得锁的线程可以多次锁住/解锁监视对象
  2. 按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用
  3. 因此偏向锁的想法是一旦线程第一次获得了监视器锁对象,之后让监视对象“偏向”这个 线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程

CAS会导致Cache一致性流量

  1. 如果有很多线程都共享同一个对象,当某个Core CAS成功时必然会引起“Cache一致性流量”,造成本地延迟,
  2. 偏向锁,只有一个线程拥有这个对象,不需要一致性,本质上偏向锁就是为了消除CAS,降低Cache一致性流量

为什么CAS会延迟本地调用 CAS及SMP(Symmetrical Multi-Processing)架构

CAS为什么会引入本地延迟?这要从SMP(对称多处理器)架构说起,下图大概表明了SMP的结构:

其意思是所有的CPU会共享一条系统总线(BUS),靠此总线连接主存。每个核都有自己的一级缓存,各核相对于BUS对称分布,因此这种结构称为“对称多处理器”。

CAS的全称为Compare-And-Swap,是一条CPU的原子指令,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现, 其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口

 

Cache一致性流量 锁设计的终极目标

  1. Core1和Core2可能会同时把主存中某个位置的值Load到自己的L1 Cache中,当Core1在自己的L1 Cache中修改这个位置的值时,会通过总线,使Core2中L1 Cache对应的值“失效”,而Core2一旦发现自己L1 Cache中的值失效(称为Cache命中缺失)则会通过总线从内存中加载该地址最新的值,
  2. 大家通过总线的来回通信称为“Cache一致性流量”,
  3. 因为总线被设计为固定的“通信能力”,如果Cache一致性流量过大,总线将成为瓶颈。而当Core1和Core2中的值再次一致时,称为“Cache一致性”,从这个层面来说,锁设计的终极目标便是减少Cache一致性流量
  4.  
  5. 偏向锁:先记住,新线程来时,CAS 替换Thead ID,失败则说明存在竞争,升级为轻量级锁
  6. 升级过程:偏向可能直接到重量
    1. 原线程到达安全点后,原栈中分配锁记录,copy对象头的mark word到锁记录,mw指针指向锁记录,owner指向mw;唤醒原线程继续执行
  7. 解锁:原线程检查mw的指针是否还指向自己,如果不是,说明有线程在这期间换掉mw指针了并阻塞了,唤醒等待的线程【此时已经是重量级锁了】
  8. 期间:
    1. 竞争线程在在栈中分配锁记录,copy,mw指针互相定位,如果失败,自旋,自旋失败则膨胀成重量锁
    2. 自旋成功,说明只是短暂竞争,大家相安无事

偏向解除

偏向锁引入的一个重要问题是,在多争用的场景下,如果另外一个线程争用偏向对象,拥有者需要释放偏向锁,

而释放的过程会带来一些性能开销,但总体说来偏向锁带来的好处还是大于CAS代价的

 

synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,非公平性,但获得了高吞吐量。

 

偏向锁升级为轻量级锁

对象头默认无锁状态存储01,运行期间根据锁状态不同会存储不同的内容;64位

 

无锁->偏向锁获取过程

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
  2. 如果为可偏向状态,则判断Mark Word偏向线程ID是否指向当前线程,如果是,执行同步代码
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行同步代码;

偏向锁升级为轻量级锁

 

  1. 如果竞争失败,CAS获取偏向锁失败,表示有竞争,撤销偏向锁,导致stop the word
  2. 当原线程到达全局安全点(safepoint)时,检查是否退出同步代码块,若否,偏向锁升级为轻量级锁【开始拷贝mark word到锁记录】
  3. 拷贝完成后,被阻塞在安全点的原线程继续往下执行同步代码

特点:

对象头记录线程ID,适用于只有一个线程访问同步块场景,撤销时stop word

适用只有一个线程适用锁的情况

 

轻量级锁 CAS乐观锁

  1. 轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS
  2. 轻量级锁,无实际竞争,多个线程交替使用锁、允许短时间【50次自旋】的锁竞争
  3. 通过mark word和锁记录的指针指向和一致性实现【因为对象需要记住多个线程,不能用偏向ID了】,否则CAS,失败则重量级锁
  1. 虚拟机首先将在当前线程的私有栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝【因为栈帧为线程私有,对象大家都有】
  2. 拷贝对象的对象头中的Mark Word复制到线程的锁记录(Lock Record)中;
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的对象头中的mark word字段中的指针,更新为指向线程锁记录指针【表示对象是这个线程的了】,并将锁记录里的owner指针指向对象 mark word【配对成功】。如果更新成功,则执行步骤4,否则升级重量锁
    1. 即对象与线程关系:对象头的锁记录指针【mark word中】是否指向当前线程【栈中的锁记录】
  4. 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示

轻量级锁的释放 CAS操作

对象头中mark word指针是否指向当前线程锁记录【对象->锁记录】 且&&

拷贝在当前线程锁记录的mark word信息是否与对象头的mark word一致

若是,释放锁,若否,说明有线程被挂起,唤醒挂起的线程开始竞争切换

 

轻量级锁升级为重量级锁

  1. 如果CAS更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧中的锁记录,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行
  2. 否则,进行自旋锁优化,当前线程尝试使用自旋来获取锁,循环去获取锁。自旋+自适应自旋
  3. 自旋获取锁失败后,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁monitor的指针【是否在栈中的互斥量?】,后面等待锁的线程也要进入阻塞状态

 

重量级锁

有实际竞争,且锁竞争时间长

自旋获取锁失败后,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁的指针监视器monitor指针【是否在栈中的锁记录】,后面等待锁的线程也要进入阻塞状态

 

自旋锁

阻塞影响锁的性能

那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态

阻塞操作由操作系统完成(在Linxu下通 过pthread_mutex_lock函数),线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能

自旋

缓解上述问题的办法便是自旋,其原理是:当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程可以稍微等一等(自旋), 在Owner线程释放锁后,争用线程可能会立即得到锁,【不需要去竞争队列排队】,从而避免了系统阻塞。

但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后还是无法 获得锁,这时争用线程则会停止自旋进入阻塞状态,加入竞争队列(后退)

这对那些执行时间很短的代码块来说有非常重要的性能提高。自旋在多处理器上才有意义。

还有个问题是,线程自旋时做些啥?其实啥都不做,可以执行几次for循环,可以执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机 会。所以说,自旋是把双刃剑,如果旋的时间过长会影响整体性能,时间过短又达不到延迟阻塞的目的。显然,自旋的周期选择显得非常重要,但这与操作系统、硬 件体系、系统的负载等诸多场景相关,很难选择,如果选择不当,不但性能得不到提高,可能还会下降,因此大家普遍认为自旋锁不具有扩展性

自旋优化策略

对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并没有做到。经过调查,目前只是通过汇编暂停了几个CPU周期,除了自旋周期选择,HotSpot还进行许多其他的自旋优化策略,具体如下:

如果平均负载小于CPUs则一直自旋

如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞

如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞

如果CPU处于节电模式则停止自旋

自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)

自旋时会适当放弃线程优先级之间的差异

 

如果线程尝试获取锁的时候,轻量锁正被其他线程占有,那么它就会修改markword,修改重量级锁,表示该进入重量锁了

 

synchronized 的不公平,实现何时使用了自旋锁

 

在线程进入ContentionList时,也即第一步操作前。线程在进入等待队列时 首先进行自旋尝试获得锁【自旋copy对象markword到锁记录】,如果不成功再进入等待队列

这对那些已经在等待队列中的线程来说,稍微显得不公平

还有一个不公平的地方是自旋线程可能会抢占了 Ready【OnDeck】线程的锁。自旋锁由每个监视对象维护,每个监视对象一个

尝试获取锁是指:对象头的锁记录指针【mark word中】是否指向当前线程【栈中的锁记录】

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值