☰
Java锁实现方式全解析:从synchronized到AQS与CAS
2026/10/20 16:46:21 网站建设 项目流程

1. 从synchronized开始:字节码、Mark Word与Monitor

1.1 javap底下的monitorenter与monitorexit

先看一段最常见不过的代码:

public class LockDemo { public void test() { synchronized (this) { System.out.println("hello"); } } }

用javap -p -c LockDemo反编译后,关键部分长这样:

0: aload_0 1: dup 2: monitorenter 3: getstatic // System.out 6: ldc // "hello" 8: invokevirtual // println 11: aload_0 12: monitorexit 13: goto 21 16: astore_1 17: aload_0 18: monitorexit 19: aload_1 20: athrow 21: return

所谓Java锁实现方式,最底层的第一层就从这里开始:synchronized块的入口和出口分别对应monitorenter和monitorexit两条字节码指令。需要注意,方法正常退出只执行一次monitorexit,但JVM还会在编译阶段把异常路径处理好——上面的异常表告诉我们,如果第3到13行执行期间抛了异常,会跳到16行重新执行monitorexit,保证锁一定会被释放,不会因为业务代码抛异常导致死锁。

这个设计看起来简单,但它已经奠定了后续所有问题的基调:锁是幂等可重入的,同一个线程可以多次进入同一个监视器;锁的释放是编译期保证的,不需要开发者在 finally 里手动释放。

我在实际排查问题的时候,经常让团队先把反编译结果打开。因为很多所谓的并发死锁,其实并不是synchronized本身的 bug,而是业务代码在锁块内部调用了外部RPC、等待IO或者嵌套了其他锁,导致持有锁时间过长。看到字节码里那一对monitorenter/monitorexit,再对照线程栈,定位起来会快很多。

1.2 Mark Word:对象头里怎么记录锁状态

如果说monitorenter是锁在字节码层面的入口,那么真正记录锁状态的,是每个Java对象头里的Mark Word。JVM对对象内存布局做了大量优化,不会为每个对象单独分配一把锁的数据结构,而是在对象头里留出一段固定长度的空间,用来保存哈希值、GC分代年龄和锁状态标志。

在64位JVM上,Mark Word一般是64bit。不同锁状态下,这64bit的用途完全不一样:

锁状态Mark Word存储内容锁标志位
无锁对象哈希码(31bit)、GC分代年龄(4bit)等01
偏向锁持有锁的线程ID(54bit)、偏向时间戳、GC分代年龄01
轻量级锁指向栈帧中Lock Record的指针00
重量级锁指向ObjectMonitor监视器对象的指针10
GC标记空(不保存业务信息)11

这里有个容易混淆的地方:无锁和偏向锁的标志位都是01,区分它们要看偏向锁标志位(倒数第3位)。所以JVM判断一个对象的锁状态,是先读标志位,再判断是否处于偏向状态,而不是像很多人以为的那样"看到01就是无锁"。

理解Mark Word之后,很多现象都能解释了。比如为什么hashCode()和偏向锁经常"打架":一旦对象计算过 identity hashcode,哈希值就会永久写入Mark Word,那这块空间就再也存不下偏向锁的线程ID了。所以JVM在实际运行中,如果一个对象先调用了hashCode(),它往往直接跳过偏向锁,进入轻量级锁或重量级锁流程。这个细节在锁升级分析里非常容易被忽略,却会实打实影响性能表现。

1.3 ObjectMonitor:重量级锁的数据结构

当锁竞争升级到一定程度,Mark Word里的锁标志位会变成10,指向一个真正的监视器对象,也就是重量级锁。很多人把"重量级锁"想象成一把很重的锁,其实准确说,它是唯一真正依赖操作系统线程原语来阻塞和唤醒线程的锁。

这个监视器对象内部维护了这么几个关键字段:

  • _owner:当前持有锁的线程
  • _recursions:重入计数,同一个线程重复获取锁时累加,释放时递减
  • _EntryList:正在竞争锁、但还没拿到锁的线程队列
  • _WaitSet:调用过wait()进入等待状态的线程队列
  • cxq:竞争失败后线程快速入队的单向队列

线程执行monitorenter时,如果发现锁已被其他线程持有,不是立刻挂起,而是先在 cxq 排队,通过CAS来抢锁。抢不到才会真正把线程挂起,等待锁持有者释放时被唤醒。阻塞和唤醒涉及用户态到内核态的切换,成本很高,这也是重量级锁"重量"二字的真正含义。

反过来说,为什么老版本Java里并发性能差?因为早期的synchronized无论竞争多激烈,都直接走这种阻塞模型。后来JVM意识到大部分场景锁竞争并没有想象中激烈,甚至大量锁只被单个线程反复获取,于是才有了锁升级的整套优化。这部分内容,值得单独展开讲。

2. 锁升级链路:偏向锁到轻量级锁再到重量级锁

2.1 偏向锁:一次CAS换来的快速重入

锁升级的第一站是偏向锁。它的设计假设很直白:大多数锁在绝大多数时间里只被一个线程反复获取,比如某个synchronized方法只在初始化阶段被主线程调用一次,或者一个全局缓存对象只在某个固定线程内访问。

偏向锁的做法是:第一个线程进入同步块时,JVM通过CAS把Mark Word里的线程ID从0改写为当前线程ID。以后这个线程再次进入同步块,只需要检查Mark Word里的线程ID是否是自己,如果是就直接放行,连CAS都不用做了。

这串过程的收益非常可观:无竞争、单线程反复加锁,只需要一次简单的身份比对。代价是什么?偏向锁的撤销成本很高。如果有第二个线程来竞争,JVM不能像轻量级锁那样立刻升级,而是必须等一个安全点(SafePoint),暂停所有业务线程,然后判断原来持有偏向锁的线程是否还存活、是否还处于同步块中,再决定是撤销偏向锁,还是把偏向锁重新偏向给新线程。

我在压测场景里见过一个非常典型的现象:某个共享对象被多线程高频交替访问,JVM默认开启了偏向锁,结果每次切换线程都要执行一次偏向锁撤销和重偏向。安全点暂停加CAS成本,反而把性能拖得更差。后来直接把偏向锁关闭,改用轻量级锁,整体吞吐反而上去了。后面第5节我会专门说这种情况怎么判断。

偏向锁在JDK 15之后被默认禁用并标记为废弃,原因正是它的撤销逻辑在高竞争和现代应用场景下收益变小、维护成本变高。如果你还在用较早的JDK版本,遇到高竞争场景,可以考虑用-XX:-UseBiasedLocking显式关闭。

2.2 轻量级锁:栈帧里的Lock Record

当偏向锁竞争出现,且偏向锁无法继续使用,JVM会把锁升级为轻量级锁。轻量级锁的核心思路不是用操作系统阻塞线程,而是在当前线程的栈帧里分配一个Lock Record空间,然后用CAS尝试把对象头的Mark Word复制到这个Lock Record里,同时把Mark Word改造成指向Lock Record的指针。

如果CAS成功,说明当前线程抢到了锁;如果CAS失败,JVM会先检查Mark Word是不是已经指向当前线程自己的Lock Record,如果是,说明这是重入,直接计数加一即可。如果不是,说明确实有其他线程竞争,于是进入自旋。

这里值得展开说一下为什么轻量级锁能够快。因为它假设竞争不激烈,线程短时间自旋一下就能拿到锁,整个过程完全不进入内核态,不涉及线程挂起和唤醒。从操作系统的角度看,这几乎就是在用户态完成了一次锁获取。一旦假设不成立,比如多个线程长时间互相争抢,轻量级锁就会膨胀成重量级锁。

实际项目中,用jstack看到的线程状态也能验证这套模型:锁竞争不激烈时,等待线程大多是RUNNABLE转来转去,或者短暂WAITING在某个AbstractQueuedSynchronizer条件上;真到了重量级锁阶段,你才会看到大量线程停留在BLOCKED (on object monitor)状态。

2.3 自旋与膨胀:竞争加剧后的应对

轻量级锁失败后,JVM不会立刻把线程挂起,而是先让线程自旋一会儿。自旋的本质是空转CPU,反复尝试获取锁。对于锁持有时间非常短的场景,自旋能避免线程切换带来的巨大开销;但对锁持有时间长的场景,自旋会白白消耗CPU。

JDK 1.6之后引入了自适应自旋,JVM会根据同一个锁过去的自旋成功率来动态调整自旋次数。如果上次自旋成功,下次就多自旋几次;如果经常失败,就减少甚至不自旋。这个参数不需要开发者手动配置,但了解它的存在,对排查"压测时CPU飙高但线程没有阻塞"的问题很有帮助。

自旋始终是有限度的。当自旋次数超过阈值,或者有线程在自旋期间被挂起(比如调用了wait()),锁就会膨胀为重量级锁,也就是第1节说的ObjectMonitor。膨胀的过程需要把当前持有锁的线程信息迁移到监视器对象里,并把Mark Word更新为指向ObjectMonitor。一旦完成膨胀,后续所有竞争线程都走操作系统级阻塞唤醒。

有一个反直觉的经验:重量级锁并不总是"性能差"的代名词。在高竞争、长持有时间的场景里,让线程阻塞等待,反而比让一堆线程在CPU上空转更合理。真正的性能杀手不是锁类型本身,而是"锁类型和竞争模型不匹配"。

2.4 锁状态总览与JDK 15之后的变化

把上面的升级路径整理成一张表,方便对照:

锁状态适用场景竞争处理方式主要开销
偏向锁单线程反复获取同一把锁仅检查线程ID撤销时需要安全点暂停
轻量级锁多线程短时效竞争CAS + 较短自旋CAS频繁失败的CPU消耗
重量级锁多线程长时效竞争线程阻塞与唤醒用户态/内核态切换
无锁(CAS)单变量更新、短临界区CAS循环重试高竞争时CPU空转

JVM内部实际上还有锁粗化和锁消除两个编译优化。锁粗化是把相邻多个同步块合并成一个大的同步块,减少锁的获取释放次数;锁消除则是通过逃逸分析发现同步块中的对象不会逃逸出当前线程,直接把锁去掉。这两个优化不是Java语法层面的行为,但会直接影响你压测时看到的性能结果。

JDK 15之后偏向锁默认关闭,但对开发者来说,并不意味着要改写法。synchronized仍是首选,只是JVM少了一层偏向优化。我们在生产JDK 17环境下,把之前依赖偏向锁的特性重新压测了一遍,发现影响非常小,因为现代应用的服务端并发模型早已不是"单线程访问共享锁"的形态了。

3. ReentrantLock与AQS:显式锁的等待队列实现

3.1 state字段与CLH变体队列

聊完JVM层面的synchronized,再看显式锁。ReentrantLock的底层是AbstractQueuedSynchronizer,通常简称为AQS。AQS的设计哲学用一个词概括就是:把线程的排队、阻塞、唤醒这些通用逻辑抽象出来,具体某个锁的"什么条件下能拿到锁"则留给子类实现。

AQS内部最重要的两个东西是volatile int state和双向等待队列。state字段在不同锁里有不同含义:在ReentrantLock里它表示重入次数;在Semaphore里它表示剩余许可数;在CountDownLatch里它表示还需要等待多少事件完成。因此state就是AQS实现各种同步器的基础。

等待队列是一个CLH锁的变体,每个等待线程被封装成一个Node节点。Node里有prev和next指针、表示线程等待模式的nextWaiter、以及一个关键的volatile waitStatus字段。waitStatus的几个取值需要背下来:

  • CANCELLED=1:线程被取消或者中断,节点将被清理
  • SIGNAL=-1:当前节点释放锁时,需要唤醒后继节点
  • CONDITION=-2:节点在条件队列中等待
  • PROPAGATE=-3:共享锁场景下,释放过程需要向后传播

很多面试题喜欢问"ReentrantLock怎么实现阻塞等待的",答案就是这套Node和waitStatus机制。节点在排队时不会立刻阻塞,而是先把前驱节点的waitStatus设置成SIGNAL,然后才park自己。这样做的好处是,释放锁的线程能找到确切需要唤醒的后继节点,避免无谓的唤醒广播。

3.2 lock与unlock的完整路径

我用一个调用链说明ReentrantLock.lock()到底经历了什么。以非公平锁为例:

  1. lock()先直接CAS:把state从0改成1。如果成功,说明抢到了锁,设置当前线程为持有者;如果失败,进入下一步。
  2. 调用acquire(1),这是AQS的模板方法,内部先调tryAcquire(1)。
  3. tryAcquire再看一次state:如果state是0,再次CAS尝试抢锁;如果当前线程已经持有锁,state加一,重入成功。
  4. 如果tryAcquire返回false,调用addWaiter把当前线程包装成Node加入等待队列尾部。
  5. 进入acquireQueued自旋循环:检查前驱节点是不是head,是的话就再一次尝试tryAcquire;不是的话,把前驱waitStatus设为SIGNAL,然后调用LockSupport.park(this)真正挂起自己。
  6. 被唤醒后,重新检查自己是不是排在队列最前面,如果是且抢锁成功,把自己设为head,等待结束。

释放锁的路径对称简单:unlock调用release(1),state减一,如果state归零,说明完全释放,清空持有者线程;然后判断头节点的waitStatus,如果需要唤醒后继节点,调用LockSupport.unpark唤醒队首等待线程。

这段流程里最容易忽略的是:AQS队列中的所有操作,包括入队和出队,本身也依赖CAS。入队要CAS更新tail指针,出队要把head头节点清空。这解释了为什么"AQS是无锁队列实现"这个说法其实同时成立——队列的维护是全CAS的,只有"等待锁"这个语义才会让线程真正阻塞挂起。

3.3 公平性、重入与中断如何通过AQS表达

非公平锁和公平锁在AQS里的差别非常小,关键只在一个方法上。非公平锁在lock()时会先做一次CAS强抢,抢不到才排队;公平锁则强制要求tryAcquire里先调用hasQueuedPredecessors(),只要队列里还有等待者,当前线程就必须排队,不允许插队。

很多人以为非公平锁会导致线程饿死,现实中非公锁的吞吐率反而往往优于公平锁。原因是公平锁的严格排队会让大量线程频繁被park和unpark,切换成本很高;非公平锁允许"即将阻塞的线程"和"新来的线程"直接在用户态抢一把,减少了线程切换次数。因此如果你没有强实时需求,默认使用非公平锁通常更划算。

重入的实现就更直白了。Java里ReentrantLock支持同一线程重复获取同一把锁,每次tryAcquire时 state加一,每次release时 state减一,只有减到0才真正释放。很多人在业务代码里把锁释放写在finally里,其实即使不写finally,只要手动调用unlock次数和lock次数相等,逻辑也不会出错。但一旦中途抛异常导致unlock没执行,锁就永远不会被释放,所以finally仍然是必须的。

中断的处理依赖AQS里一个非常关键的设计:acquireQueued在挂起期间响应中断。线程被park期间如果收到中断信号,并不会像Thread.sleep那样立刻抛异常,而是在被唤醒后才检查中断标志位。默认行为是在循环里返回中断状态,让上层lockInterruptibly决定是否抛出InterruptedException。普通lock()会忽略中断,线程继续排队;lockInterruptibly()则直接放弃抢锁。这个差异在写可取消任务时非常重要。

3.4 Condition:另一条等待队列

Condition是ReentrantLock相对synchronized的一大优势。synchronized的wait/notify只有一个隐藏条件队列,而 AQS 里的ConditionObject为每个条件维护了独立的等待队列。

await()会把当前线程从同步队列中摘除,释放锁并构造一个CONDITION节点放入条件等待队列,然后park挂起。signal()做的事相反:把条件队列头部的节点移动到同步队列尾部,让它重新参与锁的竞争。signalAll()则是把条件队列里的所有节点全部搬到同步队列。

这套机制对应到业务上就是典型的生产者消费者模型:生产者等"队列有空位"这个条件,消费者等"队列有数据"这个条件,两个条件互相独立,不会出现惊群效应。我在早期写消息缓冲队列时,用synchronized只能全量notifyAll,所有等待线程都被唤醒,然后大部分再次陷入等待;换成ReentrantLock加双Condition后,唤醒数量从"一批"变成"恰好一个"。

这里有个使用上容易踩的坑:await之后如果线程被中断,Condition会主动抛出InterruptedException并把自己从条件队列中清除。如果你在自己的代码里把中断吞掉了,信号可能会丢失。所以建议在finally块中尽量避免对await结果的假设,始终在循环里重新检查条件。

4. 不用锁的锁实现:volatile、CAS与分段累加

4.1 volatile的"弱一致性"与可见性保证

如果把主题"Java锁实现方式"的理解停留在synchronized和 AQS 层面,就少了一半。实际并发编程中,还有一大类场景并不需要"锁住代码块",而是通过volatile保证可见性,再通过CAS保证原子性,组合起来实现无锁方案。

volatile解决的是可见性和有序性问题。写volatile变量时,JVM会插入内存屏障,确保写操作在屏障之前的普通写不会重排到屏障之后;读volatile变量时,读取会获取一个"在当前时间点能看到的最新值"。

但volatile能做的很有限。i++这样一行代码,实际是"读取i、加一、写回i"三步,volatile只能保证每一步的可见性,不能保证这三步作为一个整体被原子执行。所以并发场景下volatile int i执行一万次自增,结果会远小于一万。这也是为什么需要一个真正具备原子语义的CAS指令。

4.2 CAS在CPU层面长什么样

CAS,全称compare-and-swap,Java里通过sun.misc.Unsafe暴露给并发库使用。compareAndSwapInt的语义是:如果当前对象偏移处的值等于期望值,就把目标值写进去,返回true;否则不修改,返回false。

在x86平台上,CAS会被编译成cmpxchg指令。多核系统下,为了确保"比较并交换"过程中的原子性,还需要在前面加一条lock前缀,锁住总线或者锁缓存行。看到这里你会发现,所谓无锁其实并不是真的没有锁,只是把锁的粒度从操作系统线程调度级别,下降到了CPU指令级别。因为一条指令的成本远低于一次线程挂起唤醒,所以CAS在竞争不激烈时能获得远优于锁的性能。

CAS的典型使用方式是循环重试:

public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) { return next; } } }

这段代码用"乐观"的方式假设没有冲突,冲突了就让出CPU重新读取。问题是高竞争下,大量线程会同时自旋重试,CPU缓存行被反复失效,反而比锁更慢。这也是我后来在做高并发扣减时,放弃AtomicLong而改用LongAdder的直接原因。

4.3 从AtomicInteger到LongAdder的演进逻辑

LongAdder的设计思想是热点分离:与其让所有线程去CAS同一个base值,不如拆成多个Cell数组,每个线程往不同的Cell里累加。计算总数时,再把base和所有Cell值加起来。

对于写多读少的纯计数场景,这种设计大幅降低了竞争。秒杀场景下的预扣库存、接口级QPS统计、流量打点,都属于典型场景。与之对应的代价是,sum()需要遍历所有Cell,而且每次调用都拿到一个不保证一致快照的近似值。如果业务要求精确读取并实时获取总数,LongAdder就不合适。

这里有一个比较实用的选择标准:

  • 单线程或极低竞争:直接AtomicLong,代码简单,延迟最低
  • 多线程高竞争、读少写多:LongAdder
  • 需要强一致性读(读取时必须看到所有已提交写):AtomicLong或加锁

同样重要的是ABA问题。CAS只能比较"值现在是不是期望值",不能知道这个值是不是被其他线程从A改成B又改回A。AtomicStampedReference通过额外维护一个版本号来规避这个过程。我在实现无锁栈的过程中,因为忽略ABA问题,出现过节点被重复入栈导致的链表环,调试了很久才发现。排查的思路也很简单:一旦怀疑CAS没生效,先看数据类型是不是引用类型,引用类型的CAS一定要考虑ABA。

4.4 并发容器里的锁粒度演进与乐观读

无锁并发的思想不只体现在原子类上,也贯穿在并发容器的设计中。ConcurrentHashMap就是一个很好的观察样本。

早期版本采用分段锁,把整个哈希表分成若干个Segment,每个Segment是一把独立的ReentrantLock。理论上只要操作落到不同Segment,就能并行执行。后来版本进一步细化了锁粒度,放弃Segment,改为对每个哈希桶的头节点加锁,锁粒度从"一段"细到"一桶",同时利用CAS对table数组的引用做初始化。

从Java 7到Java 8,锁粒度由"段"变为"桶",在高扩展场景下带来的并发行提升非常明显。put操作只会锁住对应桶的首节点,读操作绝大多数是volatile读取,完全不上锁。这也是为什么即使在高并发场景下,ConcurrentHashMap的读吞吐能远超Hashtable。我在实际排查"为什么系统吞吐到了某一阈值就上不去"时,往往第一步就是检查共享集合是不是仍在使用全局锁同步,换成ConcurrentHashMap后,分位数的延迟通常就直接降下来了。

另一个容易被忽略的无锁优化是StampedLock的乐观读:它允许线程在读锁之前先做一次乐观读,不加真正的读锁,只验证版本号。如果操作期间没有写线程介入,整个读过程完全无锁;如果版本号变了,再升级为悲观读锁。这个机制让读多写少场景获得了非常大的性能收益。但要注意,StampedLock不可重入,也不支持中断(部分方法支持),使用门槛比ReadWriteLock高不少,团队使用前要把这些限制讲清楚。

5. 项目里到底选哪种锁实现:权衡与调优

5.1 synchronized还是ReentrantLock

这是Java并发被问烂的问题,但大多数答案都不够工程化。我的原则很简单:如果只是"保证代码块互斥"这个需求,优先用synchronized。理由有三:

一是写法最安全。synchronized不需要在finally里手动释放,异常路径由JVM兜底,新人写错的可能性最低。

二是现代JVM对synchronized的优化一直在持续,从偏向锁、轻量级锁到锁消除、锁粗化,再加上JDK 15后偏向锁默认关闭,两者的性能差距已经很小。压测数据也支持这个结论:在低竞争场景下,synchronized甚至因为编译优化而略快。

三是可维护性更好。看到synchronized一眼就知道临界区范围,而ReentrantLock的lock/unlock分散在不同位置,review代码时要额外留意。

反过来,当你有以下需求时,应该选择ReentrantLock:

  • 需要超时获取锁,避免线程无限等待
  • 需要可中断获取锁,配合线程池取消任务
  • 需要多个条件变量精确唤醒
  • 需要公平锁策略
  • 需要查询等待队列长度、是否被持有等信息做监控

我建议团队在代码规范里直接写明这个取舍标准,不要出现"同一段代码里既可以synchronized又可以ReentrantLock时就随机选一个"的情况。

5.2 锁粒度、锁分离与锁消除

选定了锁类型,还要考虑锁粒度。锁粒度过粗,临界区串行化严重,并发能力会受限;锁粒度过细,加锁次数变多,锁本身的获取释放成本变得显著,还可能引入更复杂的并发bug。没有绝对正解,只能靠压测。

有几种常见的降低竞争的思路可以优先考虑:

  • 缩小临界区范围:只锁真正需要保护的数据变更部分,把耗时的IO、RPC调用移到锁外
  • 读写分离:读多写少场景用ReadWriteLock或StampedLock,读读并发、读写互斥
  • 分段或分桶:把一个大锁拆成多个小锁,不同key的写操作落到不同锁上
  • 用无锁数据结构:纯计数用原子类,缓存用ConcurrentHashMap,队列用并发队列
  • 不可变对象替代锁:值对象设计成不可变,通过替换引用实现并发安全,完全不互斥

还有一类优化来自JVM,不需要写代码,却经常被忽略。逃逸分析发现同步块里的锁对象不会逃逸出当前线程时,JVM会做锁消除,直接删掉monitorenter。前提条件是对象足够小,没有发布给其他线程。这意味着,局部锁对象配合合理的对象设计,本身就不会产生竞争,你不需要把所有共享都修改成全局锁。

5.3 识别锁竞争与验证优化效果

工程上,判断一个锁到底是不是瓶颈,不是靠猜,而是靠观察。我的标准动作是压测过程中用jstack连续抓几份线程转储,统计几种线程状态的比例:

  • 大量线程RUNNABLE但没有实际产出:可能是CAS自旋在高竞争下空转
  • 大量线程BLOCKED (on object monitor):synchronized重量级锁竞争严重
  • 大量线程WAITING (parking):ReentrantLock排队等待严重

如果BLOCKED或WAITING的线程占比很高,再去看它们具体等在哪把锁上。等待点集中在同一个monitor或同一把ReentrantLock,说明热点在这把锁上,优先优化锁粒度而不是换锁类型。等得分散,说明整体竞争高,需要从架构层面重新设计共享数据的访问方式。

之前处理过一个库存扣减类的接口:大量线程卡在synchronized上,吞吐上不去。我先压测确认了热点锁,然后做两步改造:第一步把库存放在多个分桶里,每个分桶一把锁;第二步对超卖校验采用CAS重试而不是锁等待。改造后P99显著下降。这里面最关键的其实是第一步确认热点锁的过程,如果没有这一步,贸然优化可能只是把瓶颈从这把锁搬到了另一把锁上。

关于锁实现方式,这些年最大的体会是不要迷信任何单一方案。synchronized不是万能的,CAS也不是银弹,AQS更不是越高级越好。写并发代码时,多花点时间理解你面对的真实竞争模型,再决定用哪一层——这才是Java锁实现方式这门技术最值得花时间研究的部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询