Java的锁机制--偏向锁、轻量锁、自旋锁、重量锁

基础知识

线程切换代价

Java的线程是映射到操作系统的原生线程之上的,如果阻塞或唤醒一个线程就需要操作系统介入,需要在用户态和内核态之间切换,该切换会消耗大量的系统资源,因为用户态和内核态均有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递很多变量、参数给内核,内核也需要保护好用户态切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

JVM1.6之前,Synchronized会导致争不到锁的线程直接进入阻塞状态,所以说其是一个重量级的同步操作,被称为重量锁。

为了缓解上述的性能问题,JVM1.6开始,引入了偏向锁、轻量锁,其均属于乐观锁。

Mark Word

在JVM 1.6中,对象实例在堆内存中被分为3部分: 对象头、实例数据、对齐填充。

对象头的组成部分: Mark Word、指向类的指针、数组长度(可选,数组类型时才有),每个部分长度均为1个字宽,32位的JVM中,1字宽位32 bit,64位的JVM中,1字宽为64 bit。

锁升级功能主要依赖Mark Word中锁标志位和是否偏向锁标志位。

Synchronized同步锁的升级优化路径: 偏向锁->轻量级锁->重量级锁。

2.jpg

锁升级

偏向锁

偏向锁主要用来优化同一线程多次申请同一个锁的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源。

当线程1再次获取锁时,会比较当前线程ID与锁对象头Mark Word中的线程ID是否一致。

  • 如果一致,直接获取锁,无需CAS来抢占锁;
  • 如果不一致,需要查看锁对象头Mark Word中的线程是否存活:
    • 若存活,查找线程1的栈帧信息,如果线程1还需要继续持有该锁对象,那么暂停线程1(Stop-The-World),撤销偏向锁,升级为轻量级锁;如果线程1不再使用锁对象,则将锁对象设置为无锁状态(也属于锁撤销),然后重新偏向线程2;
    • 若不存活,则将锁对象设置为无锁状态(也属于锁撤销),然后重新偏向线程2。

可以看到,当持有锁的线程宕掉之后,其他请求锁的线程会检查持有锁的线程是否存活,若不存活则直接撤销锁,从而避免了死锁

在高并发场景下,当大量线程同时竞争同一个锁资源时,偏向锁会被撤销,发生STW,加大性能开销。

JVM的默认配置为: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000,即默认开启偏向锁,并且延迟4秒生效,之所以延迟,是因为JVM刚启动时竞争比较激烈。

关闭偏向锁: -XX:-UseBiasedLocking,也可以直接设置为重量级锁: -XX:+UseHeavyMonitors。

轻量锁

轻量锁适应的场景是: 各线程交替执行同步块,大部分的锁在同步周期内不存在长时间的竞争。

轻量锁在虚拟机内部是通过BasicObjectLock对象实现的,该对象内部由一个BasicLock对象_lock和一个锁对象指针_obj组成,BasicObjectLock对象放置在Java栈帧中。

在BasicLock内部还维护着displace_header字段,用于备份锁对象头部的Mark Word。

// A BasicObjectLock associates a specific Java object with a BasicLock.
// It is currently embedded in an interpreter frame.
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
 private:
  BasicLock _lock;                        // the lock, must be double word aligned
  oop       _obj;                         // object holds the lock;
};
class BasicLock VALUE_OBJ_CLASS_SPEC {
 private:
  volatile markOop _displaced_header;
};
1.jpg

当需要判断一个线程是否持有该锁对象时,只需要简单判断锁对象头的指针是否在当前线程栈地址范围即可。

加锁

线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,即BasicObjectLock对象。

创建过程如下:

  • 将锁对象头的Mark Word拷贝赋值给BasicObjectLock中BasicLock对象的_displaced_header字段;
  • 然后线程尝试使用CAS将锁对象头的Mark Word替换为指向该BasicObjectLock对象的指针;
  • 若成功,则当前线程获得锁。若失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁。

可以看到,当前线程即时获取锁失败,也不会立即阻塞挂起,而是先尝试使用自旋来获取锁。如果竞争不是很激烈,可能几次自旋,当前线程就获取到锁了,从而避免了1次线程的上下文切换。

若当前线程自旋获取锁失败,锁就会膨胀成重量锁,当前线程阻塞挂起。

解锁

轻量锁解锁时,会使用原子的CAS操作将BasicObjectLock对象备份的_displaced_header替换回到锁对象头的Mark Word。若成功,则表明没有竞争发生,解锁成功;若失败,则表明当前锁存在竞争(此时锁已经膨胀为重量锁),释放锁并唤醒阻塞的线程。

自旋锁

当锁处于轻量锁状态,且被某线程持有时,其他线程尝试获取锁失败后,不会直接阻塞挂起,而是先自旋一定次数,避免正在持有锁的线程可能在很短的时间内释放锁资源。

从JVM 1.7开始,自旋锁默认启用,自旋次数不宜设置过大(避免长时间占用CPU),-XX:+UseSpinning -XX:PreBlockSpin=10,JVM 1.7之后,默认的自旋次数由JVM根据实际系统环境灵活设置。

在锁竞争不是很激烈且锁占用的时间非常短的场景下,自旋锁可以通过减少上下文切换来提高系统性能;在锁竞争激烈或者锁占用时间较长的场景下,自旋锁会导致大量的线程一直处于CAS重试状态,造成CPU空转。

在高并发场景下,可以通过关闭自旋锁来优化系统性能: -XX:-UseSpinning。

该线程自旋之后仍旧未获取锁,则其会将锁对象升级为重量锁。未抢到锁的线程都会进入Monitor,之后会被阻塞到WaitSet中。

这里有个问题,假设持有轻量锁的线程执行同步块的时候宕掉了,则不会有释放锁并唤醒阻塞线程的动作,此时会造成死锁吗?

答案是肯定不会的,因为其他线程请求锁时,会有1个线程自旋获取锁失败后将锁升级为重量锁,然后获取重量锁的线程在释放的时候会将WaitSet中阻塞的线程唤醒,也就是说即使持有轻量级锁的线程不唤醒阻塞线程,其他持有重量级锁的线程在释放锁的时候也会唤醒WaitSet中的阻塞线程的,可谓是双重保障,哈哈

重量锁

当多个线程同时请求某个Monitor时,对象的Monitor会设置以下状态用来区分请求的线程:

  • Contention List: 所有请求锁的线程被首先放置到该竞争队列;
  • Entry List: Contention List中那些有资格成为候选人的线程会被转移到Entry List;
  • Wait Set: 调用Wait方法等被阻塞的线程会被放置到Wait Set;
  • OnDeck: 任何时刻仅能有1个线程竞争锁,该线程称为OnDeck;
  • Owner: 获取锁的线程称为Owner;
  • !Owner: 释放锁的线程。
4.jpg

EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立EntryList。Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在 Hotspot中把OnDeck的选择行为称之为“竞争切换”。

OnDeck线程获得锁后即变为Owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。

需要注意的是:当持有重量锁的线程在运行期间出错,会自动释放掉锁,从而避免死锁。

小结

锁升级的过程可以通过下面2张图来展示:

3.jpg
5.jpg

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343