核心目标:synchronized就像一把钥匙或门禁卡,它的作用是保证同一时间,只有一个线程 能进入某个特定的“房间”(临界区代码块或方法)去操作共享资源(数据),防止多个线程同时捣乱,造成数据错乱(线程不安全)。
一、锁在哪里?— 锁的是对象!
关键理解:synchronized锁住的不是代码本身,而是对象(Object)!
当你用synchronized修饰一个实例方法时,锁的是调用这个方法的当前实例对象(this)。
当你用synchronized修饰一个静态方法时,锁的是这个类的 Class对象 (比如MyClass.class)。
当你使用synchronized(object) { ... }代码块时,锁的就是你指定的那个object。
比喻:
想象一个办公室(对象实例)里有一个重要的打印机(共享资源)。
每个员工(线程)想用打印机,必须拿到这个办公室唯一的门禁卡(锁)才能开门进去打印。
synchronized方法或代码块就像那扇门,锁就是门禁卡系统。卡在谁手里,谁就能进去。
二、锁的“进化史”—— 锁升级(锁膨胀)
JVM 为了平衡性能和线程安全,设计了synchronized锁的几种状态,它会根据竞争情况自动升级(膨胀)。这个过程就像闯关,越来越“重”:
1.无锁 (No Lock):
状态:对象刚创建时,或者没有任何线程试图获取锁时。
特点:最快,没有额外开销。线程可以直接访问。
比喻:办公室门开着,里面没人,谁都可以直接进去用打印机。
2.偏向锁 (Biased Locking):
状态:假设锁总是被同一个线程获得(比如某个线程频繁访问)。
原理:
对象头里的Mark Word会记录这个线程的ID。
这个线程再来,不用做任何同步操作(CAS、操作系统调用等),直接进入,就像没有锁一样快!
如果有其他线程来尝试获取锁,偏向锁就失效了,升级为轻量级锁。
目的:优化几乎没有竞争的场景,消除单线程重复获取锁的开销。
比喻:老板(特定线程)把门禁卡长期绑定在自己工牌上。每次老板来,门禁系统自动识别他的工牌(线程ID),直接开门,超快。如果别的员工(其他线程)来了,系统发现不是老板,这套“VIP自动识别”就失效了,进入下一个状态。
3.轻量级锁 (Lightweight Lock / Thin Lock):
状态:当有少量线程竞争锁,且线程获取锁的时间很短(没有阻塞)。
原理:
线程在进入同步块前,JVM会在当前线程的栈帧中创建一个叫锁记录(Lock Record)的空间。
线程尝试使用CAS (Compare-And-Swap)操作,把对象头的Mark Word复制到自己的锁记录中,并尝试将对象头的Mark Word替换为指向锁记录的指针。
如果CAS成功:该线程获得锁。对象头状态变为轻量级锁(标记位变化),指向锁记录。
如果CAS失败:说明至少有两个线程在竞争。失败线程不会立刻阻塞,它会进行一个短暂的 自旋(Spin) —— 就是循环尝试CAS几次(空转,消耗CPU但避免阻塞)。
自旋成功:获得锁。
自旋失败 (达到一定次数或阈值)或有更多线程竞争:升级为重量级锁。
目的:避免直接使用操作系统层面的重量级互斥锁(开销大),通过 CAS 和自旋在用户态解决轻微竞争。
比喻:现在有两个员工想用打印机。系统不再自动识别,而是让大家“举手抢答”(CAS)。
员工 A 先在白板(对象头)上写上“A在用”(尝试替换指针)。
如果这时没人写,A就写成功了,进去打印。
员工B来,也想写“B在用”,发现白板已经被A改了(CAS失败)。B不死心,在门口转几圈(自旋),等A出来再马上尝试写自己的名字。
如果B转了几圈A还没出来,或者又来了员工C,系统觉得太乱了,就升级规则。
4.重量级锁 (Heavyweight Lock / Mutex Lock):
状态:当竞争激烈,或者线程持有锁时间较长,或者自旋失败时。
原理:
对象头中的Mark Word指向操作系统提供的一个真正的互斥量 (Mutex Lock)或监视器(Monitor)。
未获取到锁的线程会被操作系统挂起(Blocked),放入该锁的等待队列中。
当持有锁的线程释放锁时,操作系统会唤醒等待队列中的一个线程(具体唤醒哪个取决于策略,如公平/非公平)。
线程状态的切换(用户态 <-> 内核态)和操作系统的调度,开销非常大。
目的:应对高并发竞争。虽然慢,但能保证严格的互斥,避免CPU空转浪费。
比喻:现在打印机太抢手了,门口排长队。系统派了个保安(操作系统互斥锁)守着。
保安手里只有一张通行证(互斥量)。
拿到通行证的员工(线程)才能进去打印。
没拿到的员工(线程)不能瞎转悠浪费精力了(不自旋),保安让他们去休息室(等待队列)睡觉(阻塞)。
里面的人用完出来,把通行证还给保安。保安去休息室叫醒一个人(唤醒线程),把通行证给他。
三、核心组件——监视器 (Monitor)
概念:可以把监视器 (Monitor) 理解为一个更高级的、管理重量级锁的 管理室。每个Java对象在底层都关联着一个潜在的监视器(当升级到重量级锁时才真正用到)。
关键部分:
Owner (持有者):当前持有锁的线程。只有一个线程能成为 Owner。
Entry Set (进入集/竞争队列):当线程尝试获取锁但锁已被占用时,该线程进入Entry Set等待。它们处于阻塞(Blocked)状态,等待锁释放后被唤醒竞争。
Wait Set(等待集/等待队列):当持有锁的线程执行了 object.wait()方法时,它会释放锁并进入Wait Set,状态变为等待 (Waiting)。直到其他线程调用object.notify() / notifyAll()将其唤醒(唤醒后进入Entry Set重新竞争锁)。
比喻:
管理室(Monitor)管着打印机房。
Owner:正在用打印机的人。
Entry Set:想用打印机但暂时没卡的人,在管理室门口排队等着保安叫(阻塞)。
Wait Set:有些人(线程)虽然拿到了卡(锁),但打印中途发现缺纸了(wait()),于是把卡还给保安,自己到管理室里面的休息室(Wait Set)睡觉(Waiting)去了。等送纸的人(其他线程)来了,喊一声“纸到了!”(notify()),睡觉的人被叫醒,然后到门口(Entry Set)重新排队等卡才能继续打印。
四、底层实现关键点
1.对象头Mark Word:
Java对象在内存中的布局包含一个对象头(Object Header)。
Mark Word 是对象头的一部分,长度通常为32位或64位(取决于JVM)。
它是锁状态的核心载体!它存储了:
对象的哈希码 (HashCode)
分代年龄 (GC Age)
锁标志位 (Lock Tag):几位二进制位,标识当前锁的状态(无锁、偏向锁、轻量级锁、重量级锁)。
指向锁记录(轻量级锁)或指向 Monitor 对象(重量级锁)的指针。
锁升级的过程,本质上就是修改Mark Word中的锁标志位和相应数据的过程。
2.CAS (Compare-And-Swap):
核心操作!轻量级锁获取和释放、偏向锁撤销都依赖CAS。
它是一个原子性的CPU指令。它包含三个操作数:
内存位置 (V)
预期原值 (A)
新值 (B)
操作逻辑:“如果内存位置V的值等于预期值A,那么就把V的值更新为B。整个操作是一个不可分割的原子操作。无论是否成功,都返回V的旧值。
在锁中的应用:
获取轻量级锁时:尝试用CAS把对象头的Mark Word替换为指向自己栈帧中锁记录的指针。
释放轻量级锁时:尝试用CAS把锁记录中的Mark Word拷贝回对象头。
偏向锁撤销时:也需要CAS。
优点:避免了重量级的操作系统互斥锁,在用户态完成,速度快。
缺点:自旋会消耗CPU(忙等待)。高竞争下可能导致大量CAS失败和自旋浪费CPU。
3.自旋 (Spinning):
线程在轻量级锁竞争失败后,不会立即挂起,而是执行一个忙循环(空循环)若干次,不断尝试去获取锁(通常就是不断尝试CAS)。
目的:避免线程挂起和唤醒的巨大开销(涉及操作系统内核态切换)。因为持有锁的线程很可能很快就释放了。
风险:如果锁持有时间长,自旋就是白白浪费CPU时间。
优化 - 适应性自旋 (Adaptive Spinning):现代JVM通常很聪明。它根据之前获取该锁的自旋尝试是否成功、锁持有者的状态等历史信息,动态调整自旋的次数或时间。如果之前自旋很少成功,下次可能就少转几圈甚至不转直接升级。
4.互斥量 (Mutex) / 操作系统锁:
重量级锁的底层依赖。由操作系统内核提供(如Linux下的 futex)。
当线程获取重量级锁失败时,会被操作系统内核挂起(Blocked),放入等待队列。
当锁释放时,操作系统负责唤醒等待队列中的线程。
开销大:涉及到用户态到内核态的切换、线程上下文保存与恢复、操作系统调度,比用户态的CAS操作慢得多。
五、总结与关键点
锁对象:synchronized锁住的是对象,不是代码。
锁升级:为了性能优化,锁状态会动态变化:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁。升级不可逆。
Mark Word:对象头的关键部分,存储锁状态、线程ID、指针等信息。锁升级就是修改Mark Word。
CAS (轻量级核心):原子性的比较并交换指令,是实现轻量级锁和偏向锁撤销的基础。避免直接使用重量级锁。
自旋 (轻量级策略):竞争失败时短暂忙等待尝试,避免立即挂起。有适应性优化。
Monitor (重量级核心):管理重量级锁的“管理室”,包含 Owner、Entry Set、Wait Set。依赖操作系统互斥锁,开销最大。
适用场景:
偏向锁:单线程或近乎单线程访问同步块。
轻量级锁:低并发、线程持有锁时间短。
重量级锁:高并发、线程持有锁时间长、竞争激烈。
通俗比喻总结:
你想进一个房间(同步块)用打印机(共享资源)。
无锁:门开着,没人,直接进。
偏向锁:系统认识你(记录你ID),你刷脸直接进,超快。
轻量级锁: 门口有个小本子(Mark Word)。你写上自己名字(CAS)就能进。别人发现本子上有名字,就在门口转悠几圈(自旋)等你出来再写自己名字。
重量级锁:门口排长队,保安(操作系统)拿着唯一通行证(Mutex)。拿到才能进,没拿到的去休息室睡觉(阻塞),等保安叫醒。
理解synchronized的原理,有助于你在开发中更好地使用它,避免不必要的性能瓶颈,并理解多线程程序的运行行为。