首页 / 资讯中心 / 文章详情

Java锁机制底层原理与synchronized、AQS、CAS详解

Java锁机制底层原理与synchronized、AQS、CAS详解 ★ FEATURED ARTICLE
先说一个我在面试和带人过程中反复遇到的场景Java并发这块面试官最爱问的除了线程池就是锁机制。几乎每个人都能背出synchronized和ReentrantLock的区别可你再往下追问一句偏向锁升级到轻量级锁底层到底动了什么能讲清楚的就不多了。锁机制之所以成为八股文重灾区是因为它的知识链条特别长——从CPU指令、JVM内存模型到JDK工具类每一层都有说法而大部分人的学习只停在了最表层的结论上。这篇文章不打算做那种单纯的八股背诵我想把Java锁机制从底层到使用整个串一遍synchronized在对象头里怎么玩状态位ReentrantLock背后的AQS到底怎么排队CAS是怎么做到不加锁的锁volatile在什么场景下能替代锁、什么场景下不能。你既可以拿着它准备面试也能在真正排查并发问题的时候翻回来看看。我尽量用大白话讲原理该给代码给代码该给参数给参数都是实际能落地的东西。1. 为什么锁机制是并发八股文的重灾区1.1 锁要解决的三类问题先回到最底层。多线程同时访问共享资源归根结底会出三类问题互斥、可见性、有序性。这三个词是理解所有锁的前提也是面试连环问的起点。互斥不用多解释同一时刻只能有一个线程进入临界区。可见性说的是线程A把数据改了线程B不一定能立刻看到因为CPU缓存和主内存之间存在中间层。有序性就更隐蔽了编译器、JIT、CPU都可能为了性能打乱指令的执行顺序这在单线程下没影响多线程下就可能出事。锁这件事表面上是保护一段代码本质上是在这三个维度上建立屏障。synchronized能做到volatile只能管可见性和有序性管不了互斥。把这个底层逻辑理清了后面所有概念都不会散。1.2 一把锁可以从四个维度去看业界习惯把锁这个词按各种维度切分新手最容易懵的就是这里。我见过有人把乐观锁和轻量级锁搞混其实它们的分类维度完全不一样。维度分类典型代表一句话理解竞争态度悲观锁 / 乐观锁synchronized / CAS悲观锁认为一定冲突乐观锁假设大多不冲突获取顺序公平锁 / 非公平锁ReentrantLock 两种构造先来后到还是允许插队重入能力可重入 / 不可重入ReentrantLock / 自定义锁同一线程能否反复持有占用模式独占 / 共享ReentrantLock / 读写锁一把锁是只能一人用还是可以多人读JVM锁状态偏向锁 / 轻量级锁 / 重量级锁synchronized 的三态同一把synchronized锁的升级过程加锁方式自旋 / 阻塞CAS循环 / 重量级锁失败后是转圈重试还是挂起这个表格建议你存下来。面试的时候只要能把维度说清楚再举一个反面例子就已经超过了大多数人。1.3 八股文的正确学法我自己面试别人的时候最怕听到的答案是那种背得很顺、却经不起追问的结论。偏向锁性能高为什么高synchronized在JDK6之后优化了优化了什么答案一旦是因为JVM优化了在我这里基本就挂了。正确的学法是把每个结论往底层追问一层偏向锁效率高是因为加锁时只需要比对线程ID连CAS都不需要synchronized性能提升是因为引入了锁升级机制减少了用户态和内核态的切换次数。概念落到原理上八股文就变成了真技术。接下来我就按这个思路把锁机制一层层拆开。2. synchronized的底层宇宙对象头、Mark Word与锁升级路线2.1 每个Java对象天生带着锁的元数据synchronized锁的是对象但锁信息存在哪里答案在对象头里。每个Java对象的内存布局里最前面有一段叫Mark Word的区域用来记录对象的运行时数据hashCode、GC分代年龄、锁状态等。关键点是Mark Word是一块可复用的内存。锁状态不同它里面记录的东西就完全不同。无锁状态下它存hashCode和分代年龄变成偏向锁后腾出大部分空间存偏向的线程ID变成轻量级锁后存指向线程栈中锁记录的指针变成重量级锁后存指向ObjectMonitor的指针。所以synchronized的加锁过程本质上就是根据竞争情况不断改写对象头里那几位标志位的过程。2.2 锁状态编码一张表看懂对象头的秘密在HotSpot虚拟机里Mark Word的最低2位叫lock标志位旁边还有一个1位的biased_lock标志位组合起来就能表示四种锁状态锁状态biased_locklock标志位Mark Word里存的什么无锁001hashCode、GC分代年龄偏向锁101偏向线程ID、epoch、分代年龄轻量级锁000指向栈中Lock Record的指针重量级锁010指向ObjectMonitor的指针GC标记-11转发指针等这里有个容易踩的坑偏向锁和无锁的lock标志位都是01区别在于biased_lock那一位。所以判断一个对象是否可偏向要看完整的低三位对不对。2.3 偏向锁单线程反复加锁的绿色通道偏向锁的思想非常朴素如果一个线程反复获得同一把锁那就让这个线程偏向它之后加锁只需要检查Mark Word里记录的线程ID是不是自己是就直接进临界区连CAS都不用更不需要陷入内核态。偏向锁的获取流程大概是这样的检查Mark Word是否为可偏向状态。如果可偏向尝试通过CAS把当前线程ID写入Mark Word。如果成功当前线程获得锁后续重入只需要比对线程ID。如果CAS失败说明存在竞争需要撤销偏向。注意偏向锁的撤销不是当前线程自己做的。JVM会等到全局安全点SafePoint暂停持有偏向锁的线程再判断这个线程到底有没有退出临界区。如果已经退出了就把对象头改回无锁或轻量级锁状态如果还在临界区里跑就直接升级成重量级锁让线程真正阻塞。经验提醒偏向锁的撤销成本其实不低因为它要等安全点、暂停线程。低竞争时偏向锁省下的CAS开销未必能抵消偶尔一次撤销的开销。这也是后来JVM团队对它动刀的原因。2.4 批量重偏向与批量撤销一类对象的群体事件
阅读完成 · 觉得有帮助?
咨询建站