JUC之synchronized关键字
文章目录
- 一、synchronized核心摘要
- 1.1 synchronized是什么?
- 1.2 synchronized解决了什么问题?
- 1.3 与volatile对比
- 二、三种应用方式
- 2.1 同步实例方法
- 2.2 同步静态方法
- 2.3 同步代码块
- 2.4 锁的总结
- 2.4.1 锁的类型
- 2.4.2 关键结论
- 三、深入原理: synchronized的底层实现
- 3.1 对象结构与对象头(Object Header)
- 3.2 Monitor 机制与工作流程
- 3.3 锁升级优化:从偏向锁到重量级锁
- 3.3.1 偏向锁 (Biased Locking)
- 3.3.2 轻量级锁 (Lightweight Locking)
- 3.3 重量级锁 (Heavyweight Locking)
- 3.4 源码分析
- 3.4.1 ObjectMonitor.java
- 3.4.2 objectMonitor.hpp
- 3.4.3 objectMonitor.cpp
- 3.4.4 objectMonitor.cpp作用
- 3.5 可重入性
- 3.6 死锁
- 3.7 synchronized执行流程【重要】
- 3.8 字节码层面分析
- 四、代码示例与场景分析
- 4.1 原子性保证(计数器)
- 4.2 错误的锁对象(经典陷阱)
- 4.3 ❌ 错误用法示例
- 五、总结与最佳实践
一、synchronized核心摘要
1.1 synchronized是什么?
synchronized
是 Java 内置的、最基本的互斥同步锁。它用于控制多个线程对共享资源的访问,确保同一时刻只有一个线程可以执行特定的代码段或方法。
1.2 synchronized解决了什么问题?
它解决了并发编程中的三大核心问题:
- 原子性(Atomicity): 确保被锁定的代码块作为一个不可分割的整体执行,不会被打断。
- 可见性(Visibility): 当一个线程释放锁时,它对共享变量所做的修改必须刷新到主内存。当另一个线程获取锁时,它将看到前一个线程所做的所有修改。
- 有序性(Ordering): 由于互斥性,被同步的代码段相当于一个单线程环境,可以防止指令重排序破坏代码语义(但仅限于同步块内部,同步块外部的代码仍可能被重排序)。
1.3 与volatile对比
特性 | synchronized | volatile |
---|---|---|
互斥/原子性 | 是,可保证复合操作(如 i++ )的原子性 | 否,只能保证单次读/写的原子性 |
可见性 | 是(通过 unlock 前的写回和 lock 后的重新加载) | 是(强制读写主内存) |
有序性 | 是(as-if-serial 语义在同步块内有效) | 是(通过内存屏障禁止重排序) |
使用成本 | 重量级,涉及内核态与用户态切换(在优化后已大幅降低) | 轻量级,仅为 CPU 指令级别 |
阻塞 | 是,未获取锁的线程会进入阻塞状态 | 否,不会引起线程上下文切换 |
二、三种应用方式
2.1 同步实例方法
锁是当前对象实例(this
)。
public class SynchronizedDemo {private int count = 0;// 锁是 this,当前对象实例public synchronized void increment() {count++; // 这个操作现在是原子的}// 等效的代码块写法public void incrementEquivalence() {synchronized (this) {count++;}}
}
使用场景: 当多个线程操作同一个对象实例的同步方法时,它们会相互竞争这把锁。
2.2 同步静态方法
锁是当前类的 Class
对象(例如 SynchronizedDemo.class
)。
public class SynchronizedDemo {private static int staticCount = 0;// 锁是 SynchronizedDemo.classpublic static synchronized void staticIncrement() {staticCount++;}// 等效的代码块写法public static void staticIncrementEquivalence() {synchronized (SynchronizedDemo.class) {staticCount++;}}
}
使用场景: 当多个线程操作这个类的所有实例的静态同步方法时,它们会竞争同一把类锁。实例锁和类锁是两把不同的锁,互不干扰。
2.3 同步代码块
可以指定任意对象作为锁(lock
),灵活性最高。
public class SynchronizedDemo {private final Object lock = new Object(); // 专门的锁对象private int count = 0;public void doSomething() {// ... 一些非线程安全的操作synchronized (lock) { // 使用一个专门的对象作为锁// 临界区代码count++;// ...}// ... 另一些非线程安全的操作}
}
优势: 减小了同步范围(锁粒度),提升了性能。使用私有的、final 的锁对象可以防止外部代码意外获得你的锁,从而提高安全性。
2.4 锁的总结
2.4.1 锁的类型
-
对象锁(实例锁)
- 对象锁是针对实例对象的锁
- 每个实例对象都有自己的对象锁
- 不同实例对象的对象锁互不干扰
-
类锁(静态锁)
- 类锁是针对类的锁,即 Class 对象的锁
- 每个类只有一个类锁
- 无论创建多少个实例,类锁都是同一个
2.4.2 关键结论
- 对于普通同步方法,锁是当前实例对象(this)
- 对于静态同步方法,锁是当前类的 Class 对象
- 对于同步方法块,锁是 synchronized 括号里配置的对象
- 同一时间,一个锁只能被一个线程持有
- 对象锁和类锁互不干扰,可以同时存在
[!note]
- 一个对象当中如果有多个
synchronized
方法. 在某一个时间片内, 只要有一个线程调用了其中的某一个synchronized
方法了, 那么其它线程只能等待;
- 换句话说, 某一时刻内, 只能唯一一个线程去访问这些
synchronized
方法.- 重点记住:
synchronized
锁定的是当前对象「this」, 被锁定后,其它的线程都不能进入到当前对象的其它synchronized
方法;- 普通方法,不参与锁竞争, 所以和同步锁无关;
- 换成两个实例之后, 不是同一个对象了,那么也就不是同一把锁了;谁先谁后,并没有本质的联系了;
- 都换成静态同步方法. 锁定的是是类,而不是对象,即锁定的是「类名.class」, 即字节码对象也就是整个类「模板」;
三、深入原理: synchronized的底层实现
synchronized
的语义是通过 JVM 内部的 ObjectMonitor
(监视器锁)机制来实现的,而每个 Java 对象都与一个 ObjectMonitor
相关联。这种关联信息存储在对象的对象头(Object Header) 中。
3.1 对象结构与对象头(Object Header)
一个对象在堆内存中的布局分为三部分:
- 对象头 (Header): 存储对象的元数据,如哈希码、GC 分代年龄、锁状态标志、指向 Monitor 的指针等。
- 实例数据 (Instance Data): 存储对象的有效信息,即程序代码中定义的各种字段内容。
- 对齐填充 (Padding): 起占位符作用,确保对象大小是 8 字节的整数倍。
其中,对象头的 Mark Word 是理解锁的关键。它在 32 位和 64 位 JVM 中的长度不同(32bit / 64bit),其结构会随着锁标志位的变化而动态变化。
32 位 JVM 下 Mark Word 的存储结构:
3.2 Monitor 机制与工作流程
每个 Java 对象都潜在地带有一个 Monitor(监视器)。可以把 Monitor 理解为一个特殊的房间,这个房间一次只能容纳一个线程。
这个房间有三部分组成:
- Entry Set(入口集): 想要进入房间的线程在此等候。
- Owner(所有者): 当前正在房间内执行的线程。
- Wait Set(等待集): 曾经进入过房间但主动暂时放弃资格的线程在此等候(通过
Object.wait()
)。
Monitor 是 JVM 提供的一种同步原语,每个 Java 对象都可以关联一个 Monitor。它包含:
- Entry Set:等待获取锁的线程队列。
- Wait Set:调用
wait()
后进入等待的线程队列。 - Owner:当前持有锁的线程。
- 计数器(recursions):记录重入次数
Java 对象在堆中存储时,其对象头(Object Header)包含:
- Mark Word:存储哈希码、GC 分代年龄、锁状态等。
- Klass Pointer:指向类元数据的指针。
Mark Word 结构(以 64 位 HotSpot 为例)
锁状态 | Mark Word 内容 |
---|---|
无锁 | 哈希码 + 分代年龄 + 01 |
偏向锁 | 线程 ID + Epoch + 对象分代年龄 + 101 |
轻量级锁 | 指向栈中锁记录的指针 + 00 |
重量级锁 | 指向 Monitor 的指针 + 10 |
GC 标记 | 空(用于 GC) + 11 |
synchronized
获取锁的流程:
当线程执行到 synchronized
代码块时,JVM 会尝试让这个线程获取对象的 Monitor:
- 检查 Owner 是否为 null。
- 如果是 null,则将该线程设置为 Owner,进入临界区执行,计数器 +1。
- 如果不为 null,则说明有线程持有,当前线程进入 Entry Set 阻塞等待,计数器不变。
- 当 Owner 线程执行完毕,释放锁(Owner 置为 null,计数器 -1),并唤醒 Entry Set 中的线程来竞争锁。
- 被唤醒的线程和其他新来的线程一起竞争,竞争成功的成为新的 Owner,失败的继续阻塞。
wait()
, notify()
, notifyAll()
方法正是操作 Wait Set 中的线程。
3.3 锁升级优化:从偏向锁到重量级锁
在 JDK 1.6 之后,为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁、轻量级锁和自旋锁等优化技术。锁的状态会根据竞争情况不断升级,这个过程是不可逆的。
锁的升级路径:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
flowchart TD
A[无锁] -- 第一个线程访问 --> B[偏向锁]
B -- 出现第二个线程竞争 --> C[轻量级锁<br>(自旋优化)]
C -- 自旋超过一定次数<br>或第三个线程来竞争 --> D[重量级锁]
3.3.1 偏向锁 (Biased Locking)
核心思想: 如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁时,无需再做任何同步操作(如 CAS),直接即可获取锁。
目的: 消除数据在无竞争情况下的同步开销,提高单线程执行的性能。
升级: 一旦有另一个线程来尝试竞争这个锁,偏向模式立即宣告结束。如果持有偏向锁的线程还活着,则升级为轻量级锁;否则,锁被置为无锁状态,然后重新偏向新的线程。
⚠️ 注意:JDK 15+ 已默认禁用偏向锁,因在高并发场景下维护成本高。
3.3.2 轻量级锁 (Lightweight Locking)
核心思想: 当存在轻微的锁竞争时,线程不会立即阻塞,而是通过 CAS 操作和自旋来尝试获取锁。
工作流程:
- 在代码进入同步块时,JVM 会在当前线程的栈帧中创建一个名为锁记录(Lock Record) 的空间。
- 将对象头的 Mark Word 复制到锁记录中(称为 Displaced Mark Word)。
- 线程尝试用 CAS 将对象头的 Mark Word 替换为指向锁记录的指针。
- 如果成功,当前线程获得锁,锁标志位变为
00
。 - 如果失败,表示有其他线程竞争,当前线程会自旋(循环尝试)获取锁。
升级: 如果自旋一定次数后(JDK 1.6 引入了自适应自旋)还未能获得锁,或者有第三个线程来竞争,锁就会膨胀为重量级锁。
- 如果成功,当前线程获得锁,锁标志位变为
3.3 重量级锁 (Heavyweight Locking)
核心思想: 也就是传统的 ObjectMonitor
机制。未抢到锁的线程会直接进入阻塞状态,等待被唤醒。
特点: 开销最大,涉及操作系统内核态的互斥量(mutex)操作,会导致线程上下文切换,但能应对高强度的锁竞争。
- 触发:当自旋超过一定次数(默认 10 次,由
-XX:PreBlockSpin
控制)或等待线程较多时。 - 实现:依赖操作系统互斥量(mutex),线程阻塞,进入内核态。
- 开销大:涉及用户态/内核态切换、线程阻塞与唤醒。
3.4 源码分析
[!tip]
ObjectMonitor.java
- objectMonitor.hpp
- objectMonitor.cpp
源码目录:
- \hotspot-8aac6d08b58e\agent\src\share\classes\sun\jvm\hotspot\runtime, ObjectMonitor.java
- \hotspot-8aac6d08b58e\src\share\vm\runtime, objectMonitor.cpp
- \hotspot-8aac6d08b58e\src\share\vm\runtime, objectMonitor.hpp
管程的概念: 管程就是Monitor对象.
3.4.1 ObjectMonitor.java
package sun.jvm.hotspot.runtime;import java.util.*;import sun.jvm.hotspot.debugger.*;
import sun.jvm.hotspot.oops.*;
import sun.jvm.hotspot.types.*;
import sun.jvm.hotspot.utilities.Observable;
import sun.jvm.hotspot.utilities.Observer;public class ObjectMonitor extends VMObject {static {VM.registerVMInitializedObserver(new Observer() {public void update(Observable o, Object data) {initialize(VM.getVM().getTypeDataBase());}});}private static synchronized void initialize(TypeDataBase db) throws WrongTypeException {heap = VM.getVM().getObjectHeap();Type type = db.lookupType("ObjectMonitor");sun.jvm.hotspot.types.Field f = type.getField("_metadata");metadataFieldOffset = f.getOffset();f = type.getField("_object");objectFieldOffset = f.getOffset();f = type.getField("_owner");ownerFieldOffset = f.getOffset();f = type.getField("_stack_locker");stackLockerFieldOffset = f.getOffset();f = type.getField("_next_om");nextOMFieldOffset = f.getOffset();contentionsField = new CIntField(type.getCIntegerField("_contentions"), 0);waitersField = new CIntField(type.getCIntegerField("_waiters"), 0);recursionsField = type.getCIntegerField("_recursions");ANONYMOUS_OWNER = db.lookupLongConstant("ObjectMonitor::ANONYMOUS_OWNER").longValue();}public ObjectMonitor(Address addr) {super(addr);}public Mark header() {return new Mark(addr.addOffsetTo(metadataFieldOffset));}// FIXME// void set_header(markWord hdr);// FIXME: must implement and delegate to platform-dependent implementation// public boolean isBusy();public boolean isEntered(sun.jvm.hotspot.runtime.Thread current) {Address o = owner();if (current.threadObjectAddress().equals(o) ||current.isLockOwned(o)) {return true;}return false;}public boolean isOwnedAnonymous() {return addr.getAddressAt(ownerFieldOffset).asLongValue() == ANONYMOUS_OWNER;}public Address owner() { return addr.getAddressAt(ownerFieldOffset); }public Address stackLocker() { return addr.getAddressAt(stackLockerFieldOffset); }// FIXME// void set_owner(void* owner);public int waiters() { return (int)waitersField.getValue(this); }public Address nextOM() { return addr.getAddressAt(nextOMFieldOffset); }// FIXME// void set_queue(void* owner);public long recursions() { return recursionsField.getValue(addr); }public OopHandle object() {Address objAddr = addr.getAddressAt(objectFieldOffset);if (objAddr == null) {return null;}return objAddr.getOopHandleAt(0);}public int contentions() {return (int)contentionsField.getValue(this);}// The following four either aren't expressed as typed fields in// vmStructs.cpp because they aren't strongly typed in the VM, or// would confuse the SA's type system.private static ObjectHeap heap;private static long metadataFieldOffset;private static long objectFieldOffset;private static long ownerFieldOffset;private static long stackLockerFieldOffset;private static long nextOMFieldOffset;private static CIntField contentionsField;private static CIntField waitersField;private static CIntegerField recursionsField;private static long ANONYMOUS_OWNER;// FIXME: expose platform-dependent stuff
}
ObjectMonitor.java 是 Java 虚拟机中实现对象监视器(Monitor)的核心组件,主要用于支持 synchronized 关键字的同步机制。以下是其主要作用:
- 实现 synchronized 同步机制
- ObjectMonitor 是 JVM 层面实现 synchronized 的核心数据结构
- 它负责管理对象锁的获取、释放和等待队列
- 提供了互斥访问和线程同步的基本功能
- 管理锁状态
- 无锁状态:对象未被任何线程锁定
- 偏向锁:偏向第一个获取锁的线程,减少同步开销
- 轻量级锁:当不存在竞争时使用 CAS 操作获取锁
- 重量级锁:当存在竞争时升级为重量级锁,使用 ObjectMonitor
- 维护等待队列
- 管理阻塞等待锁的线程队列(Entry Set)
- 管理调用 wait() 方法后进入等待状态的线程队列(Wait Set)
- 实现线程的阻塞和唤醒机制
3.4.2 objectMonitor.hpp
src\share\vm\runtime\objectMonitor.cpp
src\share\vm\runtime\objectMonitor.hpp
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor()
{_header = NULL;_count = 0;_waiters = 0,_recursions = 0;_object = NULL;_owner = NULL;_WaitSet = NULL;_WaitSetLock = 0;_Responsible = NULL;_succ = NULL;_cxq = NULL;FreeNext = NULL;_EntryList = NULL;_SpinFreq = 0;_SpinClock = 0;OwnerIsThread = 0;_previous_owner_tid = 0;
}bool try_enter(TRAPS);
void enter(TRAPS);
void exit(bool not_suspended, TRAPS);
void wait(jlong millis, bool interruptable, TRAPS);
void notify(TRAPS);
void notifyAll(TRAPS);
每一个对象创建的时候,都会带着一个对象监视器「管程」
每一个被锁住的对象都会和Monitor关联起来.
3.4.3 objectMonitor.cpp
try_enter
bool ObjectMonitor::try_enter(Thread *THREAD)
{if (THREAD != _owner){if (THREAD->is_lock_owned((address)_owner)){assert(_recursions == 0, "internal state error");_owner = THREAD;_recursions = 1;OwnerIsThread = 1;return true;}if (Atomic::cmpxchg_ptr(THREAD, &_owner, NULL) != NULL){return false;}return true;}else{_recursions++;return true;}
}
enter
void ATTR ObjectMonitor::enter(TRAPS)
{// The following code is ordered to check the most common cases first// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.Thread *const Self = THREAD;void *cur;cur = Atomic::cmpxchg_ptr(Self, &_owner, NULL);if (cur == NULL){// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.assert(_recursions == 0, "invariant");assert(_owner == Self, "invariant");// CONSIDER: set or assert OwnerIsThread == 1return;}if (cur == Self){// TODO-FIXME: check for integer overflow! BUGID 6557169._recursions++;return;}if (Self->is_lock_owned((address)cur)){assert(_recursions == 0, "internal state error");_recursions = 1;// Commute owner from a thread-specific on-stack BasicLockObject address to// a full-fledged "Thread *"._owner = Self;OwnerIsThread = 1;return;}// We've encountered genuine contention.assert(Self->_Stalled == 0, "invariant");Self->_Stalled = intptr_t(this);// Try one round of spinning *before* enqueueing Self// and before going through the awkward and expensive state// transitions. The following spin is strictly optional ...// Note that if we acquire the monitor from an initial spin// we forgo posting JVMTI events and firing DTRACE probes.if (Knob_SpinEarly && TrySpin(Self) > 0){assert(_owner == Self, "invariant");assert(_recursions == 0, "invariant");assert(((oop)(object()))->mark() == markOopDesc::encode(this), "invariant");Self->_Stalled = 0;return;}assert(_owner != Self, "invariant");assert(_succ != Self, "invariant");assert(Self->is_Java_thread(), "invariant");JavaThread *jt = (JavaThread *)Self;assert(!SafepointSynchronize::is_at_safepoint(), "invariant");assert(jt->thread_state() != _thread_blocked, "invariant");assert(this->object() != NULL, "invariant");assert(_count >= 0, "invariant");// Prevent deflation at STW-time. See deflate_idle_monitors() and is_busy().// Ensure the object-monitor relationship remains stable while there's contention.Atomic::inc_ptr(&_count);JFR_ONLY(JfrConditionalFlushWithStacktrace<EventJavaMonitorEnter> flush(jt);)EventJavaMonitorEnter event;if (event.should_commit()){event.set_monitorClass(((oop)this->object())->klass());event.set_address((uintptr_t)(this->object_addr()));}{ // Change java thread status to indicate blocked on monitor enter.JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);Self->set_current_pending_monitor(this);DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);if (JvmtiExport::should_post_monitor_contended_enter()){JvmtiExport::post_monitor_contended_enter(jt, this);// The current thread does not yet own the monitor and does not// yet appear on any queues that would get it made the successor.// This means that the JVMTI_EVENT_MONITOR_CONTENDED_ENTER event// handler cannot accidentally consume an unpark() meant for the// ParkEvent associated with this ObjectMonitor.}OSThreadContendState osts(Self->osthread());ThreadBlockInVM tbivm(jt);// TODO-FIXME: change the following for(;;) loop to straight-line code.for (;;){jt->set_suspend_equivalent();// cleared by handle_special_suspend_equivalent_condition()// or java_suspend_self()EnterI(THREAD);if (!ExitSuspendEquivalent(jt))break;//// We have acquired the contended monitor, but while we were// waiting another thread suspended us. We don't want to enter// the monitor while suspended because that would surprise the// thread that suspended us.//_recursions = 0;_succ = NULL;exit(false, Self);jt->java_suspend_self();}Self->set_current_pending_monitor(NULL);// We cleared the pending monitor info since we've just gotten past// the enter-check-for-suspend dance and we now own the monitor free// and clear, i.e., it is no longer pending. The ThreadBlockInVM// destructor can go to a safepoint at the end of this block. If we// do a thread dump during that safepoint, then this thread will show// as having "-locked" the monitor, but the OS and java.lang.Thread// states will still report that the thread is blocked trying to// acquire it.}Atomic::dec_ptr(&_count);assert(_count >= 0, "invariant");Self->_Stalled = 0;// Must either set _recursions = 0 or ASSERT _recursions == 0.assert(_recursions == 0, "invariant");assert(_owner == Self, "invariant");assert(_succ != Self, "invariant");assert(((oop)(object()))->mark() == markOopDesc::encode(this), "invariant");// The thread -- now the owner -- is back in vm mode.// Report the glorious news via TI,DTrace and jvmstat.// The probe effect is non-trivial. All the reportage occurs// while we hold the monitor, increasing the length of the critical// section. Amdahl's parallel speedup law comes vividly into play.//// Another option might be to aggregate the events (thread local or// per-monitor aggregation) and defer reporting until a more opportune// time -- such as next time some thread encounters contention but has// yet to acquire the lock. While spinning that thread could// spinning we could increment JVMStat counters, etc.DTRACE_MONITOR_PROBE(contended__entered, this, object(), jt);if (JvmtiExport::should_post_monitor_contended_entered()){JvmtiExport::post_monitor_contended_entered(jt, this);// The current thread already owns the monitor and is not going to// call park() for the remainder of the monitor enter protocol. So// it doesn't matter if the JVMTI_EVENT_MONITOR_CONTENDED_ENTERED// event handler consumed an unpark() issued by the thread that// just exited the monitor.}if (event.should_commit()){event.set_previousOwner((uintptr_t)_previous_owner_tid);event.commit();}if (ObjectMonitor::_sync_ContendedLockAttempts != NULL){ObjectMonitor::_sync_ContendedLockAttempts->inc();}
}
try_lock()
int ObjectMonitor::TryLock(Thread *Self)
{for (;;){void *own = _owner;if (own != NULL)return 0;if (Atomic::cmpxchg_ptr(Self, &_owner, NULL) == NULL){// Either guarantee _recursions == 0 or set _recursions = 0.assert(_recursions == 0, "invariant");assert(_owner == Self, "invariant");// CONSIDER: set or assert that OwnerIsThread == 1return 1;}// The lock had been free momentarily, but we lost the race to the lock.// Interference -- the CAS failed.// We can either return -1 or retry.// Retry doesn't make as much sense because the lock was just acquired.if (true)return -1;}
}
3.4.4 objectMonitor.cpp作用
objectMonitor.cpp
文件实现了Java对象监视器(Object Monitor)的核心功能:
- 同步机制实现:提供Java synchronized关键字的底层实现
- 线程排队管理:管理等待获取锁的线程队列(EntryList和CXQ)
- 等待/通知机制:实现Object.wait()和Object.notify()等方法
- 内存可见性保障:通过内存屏障确保多线程环境下的数据一致性
- 平台适配:针对不同操作系统的特定优化实现
3.5 可重入性
同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动释放锁「锁的对象得是同一个对象」
, 不会因为之前已经获取到锁而没有释放进而发生阻塞.
例如: 有一个synchronized修饰的的递归方法,程序第2次进入的时候,会自动获取该方法的锁,而不会阻塞住.
ReentrantLock和synchronized都是可重入锁,可重入锁在一定程度上可以避免死锁.
synchronized
是可重入锁,同一线程可以多次获取同一把锁,不会导致死锁。
可重入锁的分类
- 隐式锁: 「synchronized关键字使用的锁」, 默认是可重入锁
- 同步代码块
- 同步方法
- 简单来说,指的是可重复递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不会发生死锁,这样的锁称为可重入锁,即在一个synchronized修饰的方法或者是代码块的内部调用本类的其它synchronized修饰的方法或者代码块时,是永远可以得到锁的.
- 显示锁
- Lock锁.例如ReentrantLock
示例代码:
// 同步代码块, 可重入锁
public class T0 {public static void main(String[] args) {final Object lock = new Object();new Thread(() ->{// 同步代码块可重入锁synchronized (lock){System.out.println("同步代码块一");synchronized (lock){System.out.println("同步代码块二");synchronized (lock){System.out.println("同步代码块三");}}}}, "线程A: ").start();}
}// 同步方法
public synchronized void m1(){System.out.println("m1 method");m2();
}public synchronized void m2(){System.out.println("m2 method");
}public static void main(String[] args) {new T0().m1();
}
可重入原理:Monitor 中有一个计数器(count),线程获取锁时计数器加 1,释放锁时计数器减 1,当计数器为 0 时才真正释放锁。
3.6 死锁
package cn.tcmeta.demo04;import java.util.concurrent.TimeUnit;public class T1 {public static void main(String[] args) {final Object lock1 = new Object();final Object lock2 = new Object();new Thread(() ->{synchronized (lock1){System.out.println(Thread.currentThread().getName() + " ----- " + "持有锁对象lock1");try {TimeUnit.MILLISECONDS.sleep(2000);}catch (Exception e){e.printStackTrace();}synchronized (lock2){System.out.println(Thread.currentThread().getName() + " ----- " + "拿到了锁对象lock2");}}}, "线程A: ").start();new Thread(() ->{synchronized (lock2){System.out.println(Thread.currentThread().getName() + " ----- " + "持有锁对象lock2");try {TimeUnit.MILLISECONDS.sleep(2000);}catch (Exception e){e.printStackTrace();}synchronized (lock1){System.out.println(Thread.currentThread().getName() + " ----- " + "拿到了锁对象lock1");}}}, "线程B: ").start();}
}
排查死锁问题:
- jps, 查看当前运行的java进程
- jstack 进程id, 查看当前java的堆栈信息
3.7 synchronized执行流程【重要】
3.8 字节码层面分析
我们来看一个简单的 synchronized
方法的字节码:
public class SyncDemo {public synchronized void syncMethod() {System.out.println("Hello");}
}
使用 javap -c SyncDemo
查看字节码:
public synchronized void syncMethod();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZED // 注意这个标志Code:0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String Hello5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return
🔍 关键点:
- 方法被标记为
ACC_SYNCHRONIZED
,JVM 在调用时自动尝试获取对象锁。 - 方法执行完毕或抛出异常时,自动释放锁。
而对于同步代码块:
synchronized (this) {System.out.println("Hello");
}
字节码会生成 monitorenter
和 monitorexit
指令
0: aload_01: monitorenter // 获取锁2: getstatic #25: ldc #37: invokevirtual #4
10: monitorexit // 释放锁
11: goto 19
14: astore_1
15: monitorexit // 异常路径也必须释放锁
16: aload_1
17: athrow
✅ monitorexit
必须成对出现(正常路径 + 异常路径),由编译器自动插入,确保锁一定被释放。
四、代码示例与场景分析
4.1 原子性保证(计数器)
public class SafeCounter {private int count = 0;private final Object lock = new Object(); // 显式锁对象// 方式1: 同步方法public synchronized void incrementByMethod() {count++;}// 方式2: 同步代码块(更灵活,粒度更细)public void incrementByBlock() {// ... 一些不需要同步的准备工作synchronized (lock) {count++;}// ... 一些不需要同步的收尾工作}public int getCount() {return count;}public static void main(String[] args) throws InterruptedException {SafeCounter counter = new SafeCounter();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.incrementByBlock();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.incrementByBlock();}});t1.start();t2.start();t1.join();t2.join();System.out.println("Final count: " + counter.getCount()); // 总是 20000}
}
4.2 错误的锁对象(经典陷阱)
public class WrongLockDemo {private static final int NUM_THREADS = 100;private static int sharedCount = 0;// 锁对象是 Integer,但注意 Integer 有缓存池!private static final Integer LOCK = 0; public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[NUM_THREADS];for (int i = 0; i < NUM_THREADS; i++) {threads[i] = new Thread(() -> {// 严重问题: Integer 是 immutable 的,每次 ++ 操作都会创建一个新对象。// 不同线程可能锁的是不同的对象实例,导致同步完全失效!synchronized (Integer.valueOf(LOCK + (int) (Math.random() * 10))) { for (int j = 0; j < 100; j++) {sharedCount++;}}});threads[i].start();}for (Thread t : threads) {t.join();}// 结果大概率远小于 100 * 100 * 100System.out.println("Result with bad lock: " + sharedCount); }
}
正确做法: 锁对象应该是 private final
的,并且是一个不可变的、不会被意外复用的对象(如 private final Object lock = new Object();
)。
4.3 ❌ 错误用法示例
public class BadSync {public synchronized void methodA() { /* ... */ }public synchronized void methodB() { /* ... */ }
}
两个方法共用 this
锁,即使逻辑无关也会相互阻塞。
✅ 正确做法:细粒度锁
public class GoodSync {private final Object lockA = new Object();private final Object lockB = new Object();public void methodA() {synchronized (lockA) { /* ... */ }}public void methodB() {synchronized (lockB) { /* ... */ }}
}
五、总结与最佳实践
- 理解锁的范围: 同步代码块的粒度越小,性能越好。只同步真正需要同步的临界区。
- 谨慎选择锁对象:
- 对于实例方法,锁是
this
,要确保所有竞争线程操作的是同一个实例。 - 对于静态方法,锁是
Class
对象。 - 最推荐的方式是使用一个私有的、final 的普通 Object 对象作为显式锁。
- 对于实例方法,锁是
- 避免死锁: 确保多个线程以一致的顺序获取多个锁。例如,线程 A 先锁 X 再锁 Y,那么线程 B 也应该是先锁 X 再锁 Y,而不是先锁 Y 再锁 X。
- 考虑性能: 在低竞争场景下,
synchronized
经过锁升级优化后性能已经非常优秀。在高竞争场景下,可以考虑java.util.concurrent.locks.ReentrantLock
,它提供了更灵活的机制(如可中断、超时、公平锁等),但需要手动释放锁。 - 优先使用并发容器: 在很多场景下,使用
ConcurrentHashMap
、CopyOnWriteArrayList
等并发容器比你自己用synchronized
同步普通容器更高效、更安全。
synchronized
是 Java 并发世界的基石,深入理解其原理和最佳实践,是编写正确、高效多线程程序的关键。
- 在日常开发中,优先使用
synchronized
解决线程安全问题;- 只有在需要可中断、超时、公平性等高级特性时,才考虑
ReentrantLock
。