synchronized 是 Java 中最基础的线程同步关键字,虽然使用简单,但在实际开发中若忽略细节,容易导致 线程安全失效、性能瓶颈、死锁 等问题。以下是使用 synchronized 时必须注意的核心细节:
一、锁的对象选择:明确 "锁什么"(最易出错)
synchronized 的核心是 锁定 "对象" ,而非代码块或方法本身。锁的对象决定了同步范围,选不对会直接导致线程安全失效。
1. 实例方法锁(synchronized void method())
-
锁对象 :当前实例(
this)。 -
细节 1:不同实例的锁相互独立,多线程操作不同实例时不会同步。
java
运行
javaclass Counter { private int count = 0; public synchronized void increment() { count++; } // 锁 this } // 错误场景:多线程操作不同实例,count 会出错 Counter c1 = new Counter(); Counter c2 = new Counter(); // 线程1操作 c1,线程2操作 c2,两者无锁竞争,count 可能统计不准确 -
细节 2:若实例被回收,锁会失效(需避免锁对象被频繁创建 / 销毁)。
2. 静态方法锁(synchronized static void method())
-
锁对象 :当前类的
Class对象(全局唯一)。 -
细节:所有实例共享同一把锁,即使操作不同实例,也会同步。适合需要 "全局唯一" 同步的场景(如单例模式、全局计数器)。
java
运行
javaclass Counter { private static int globalCount = 0; public synchronized static void increment() { globalCount++; } // 锁 Counter.class } // 正确场景:多线程操作不同实例,也会同步 globalCount Counter c1 = new Counter(); Counter c2 = new Counter(); // 线程1调用 c1.increment(),线程2调用 c2.increment(),会竞争同一把锁
3. 代码块锁(synchronized(lockObj) {})
-
锁对象:自定义的锁对象(需保证多线程共享同一实例)。
-
核心细节:
-
锁对象必须是 引用类型 (不能是基本类型,如
int、long,会自动装箱为不同对象)。java
运行
javascript// 错误:锁对象是基本类型,每次自动装箱为新 Integer 对象,锁失效 int lock = 0; synchronized(lock) { ... } // 正确:使用自定义引用类型,确保多线程共享同一实例 Object lock = new Object(); // 全局唯一锁对象 synchronized(lock) { ... } -
避免使用
String、Integer等常量 / 缓存对象作为锁(可能因常量池复用导致锁冲突)。java
运行
javascript// 错误:"lock" 是常量池对象,其他地方若也用 "lock" 作为锁,会导致无关代码同步 synchronized("lock") { ... } -
锁对象不能为
null(会抛出NullPointerException)。
-
二、锁的粒度:避免 "过度同步" 或 "同步不足"
锁的粒度直接影响性能,需在 "线程安全" 和 "性能" 之间平衡。
1. 避免 "锁粒度太大"(过度同步)
-
问题:将无关的操作都放在同一个
synchronized块中,导致线程长时间阻塞,性能下降。 -
优化:仅对 "共享变量的修改 / 访问" 进行同步,缩小同步块范围。
java
运行
csharp// 错误:同步块包含无关操作(日志打印、本地变量计算),线程阻塞时间长 public synchronized void update() { log.info("开始更新"); // 无关操作,无需同步 int temp = 1 + 2; // 本地变量,无需同步 sharedVar = temp; // 仅这一步需要同步 } // 正确:缩小同步块,仅包裹临界区 public void update() { log.info("开始更新"); int temp = 1 + 2; synchronized(lock) { sharedVar = temp; // 仅临界区同步 } }
2. 避免 "锁粒度太小"(同步不足)
-
问题:若多个操作需要原子性(如 "检查 - 修改 - 提交"),仅同步单个步骤会导致线程安全问题。
-
示例:
java
运行
typescript// 错误:check 和 set 未原子化,多线程下可能重复初始化 private volatile Object instance; public Object getInstance() { if (instance == null) { // 检查(未同步) synchronized(lock) { instance = new Object(); // 修改(同步) } } return instance; } // 正确:双重检查锁(需配合 volatile 禁止重排序) public Object getInstance() { if (instance == null) { synchronized(lock) { if (instance == null) { // 二次检查(同步内) instance = new Object(); } } } return instance; }
三、线程安全的边界:synchronized 不能跨方法 / 跨线程传递
synchronized 仅保证 同一锁对象 下的同步,跨方法、跨线程时无法保证线程安全。
1. 跨方法同步失效
-
问题:若一个方法同步,另一个方法修改同一共享变量但不同步,会导致线程安全问题。
java
运行
csharpclass Counter { private int count = 0; public synchronized void increment() { count++; } // 同步 public void decrement() { count--; } // 未同步,跨方法修改共享变量 } // 线程1调用 increment(),线程2调用 decrement(),count 会出错
2. 跨线程同步失效
- 问题:
synchronized仅对 "竞争同一锁" 的线程有效,若线程间通过异步回调、消息队列等方式交互,同步无效。 - 示例:线程 A 将数据存入共享变量,线程 B 通过消息队列通知后读取,但未同步,可能读取到旧值。
四、死锁风险:避免循环等待
synchronized 可能导致死锁,需满足 四个必要条件:互斥、请求与保持、不可剥夺、循环等待。核心是避免 "循环等待"。
1. 死锁示例
java
运行
scss
// 线程1:先锁 a,再锁 b
synchronized(a) {
Thread.sleep(100); // 给线程2争取时间
synchronized(b) { ... }
}
// 线程2:先锁 b,再锁 a
synchronized(b) {
Thread.sleep(100);
synchronized(a) { ... }
}
- 结果:线程 1 持有 a 等待 b,线程 2 持有 b 等待 a,互相阻塞,死锁。
2. 避免死锁的细节
- 按 固定顺序 申请锁(如按对象哈希值排序)。
- 避免嵌套锁(尽量减少锁的嵌套层级)。
- 给锁添加 超时时间 (可通过
Lock接口的tryLock(timeout)实现,synchronized本身不支持,需手动规避)。 - 减少锁的持有时间(尽快释放锁,避免线程长时间占用)。
五、可见性与有序性:synchronized 的隐含保证
synchronized 不仅保证原子性,还隐含可见性和有序性,但需注意细节:
1. 可见性保证
- 线程释放锁时,会将工作内存中的修改写回主内存;线程获取锁时,会从主内存加载最新变量值。
- 细节:仅对 "同一锁保护的共享变量" 有效,若变量未被锁保护,仍可能出现可见性问题。
2. 有序性保证
- 同步块内的指令禁止重排序,且释放锁的操作
happens-before后续获取同一锁的操作。 - 细节:同步块外的指令仍可能重排序,仅同步块内的指令有序。
六、性能相关细节
1. synchronized 的优化(JDK 1.6+)
JDK 1.6 对 synchronized 进行了优化,引入了 偏向锁、轻量级锁、重量级锁 的升级机制,避免直接使用重量级锁(操作系统互斥量)导致的性能开销。
- 细节:无需手动干预,JVM 自动优化,但需避免锁的频繁竞争(否则会升级为重量级锁,性能下降)。
2. 避免锁竞争激烈
-
若多个线程频繁竞争同一把锁,会导致线程阻塞、上下文切换,性能下降。
-
优化方案:
- 拆分锁(如
ConcurrentHashMap的分段锁思想,将大锁拆分为多个小锁)。 - 使用无锁并发工具(如
Atomic原子类、ConcurrentLinkedQueue)。 - 减少锁的持有时间(如前面提到的缩小同步块)。
- 拆分锁(如
3. 不要用 synchronized 修饰频繁调用的方法
- 若方法被高频调用(如每秒上万次),
synchronized的同步开销会被放大,建议改用无锁方案或优化锁粒度。
七、其他细节
1. synchronized 不能中断线程
-
synchronized是不可中断的,若线程在等待锁时被阻塞,只能通过以下方式结束:- 持有锁的线程释放锁(执行完同步块或抛出异常)。
- 程序终止。
-
若需要中断等待锁的线程,需使用
Lock接口的lockInterruptibly()方法。
2. synchronized 不能超时等待
synchronized没有超时机制,线程会一直等待锁,直到获取到为止。- 若需要超时等待,需改用
Lock接口的tryLock(timeout, unit)方法。
3. 静态锁与实例锁互不干扰
-
静态方法锁(锁
Class对象)和实例方法锁(锁this)是不同的锁,多线程同时调用时不会竞争。java
运行
arduinoclass Test { public synchronized static void staticMethod() {} // 锁 Test.class public synchronized void instanceMethod() {} // 锁 this } // 线程1调用 staticMethod(),线程2调用 instanceMethod(),无锁竞争
4. 异常会释放锁
- 若同步块内抛出异常,JVM 会自动释放锁,避免锁泄露(线程持有锁但崩溃,导致其他线程无法获取)。
- 细节:若需要在异常后执行特定逻辑(如资源释放),需在
finally块中处理,但锁的释放无需手动操作。
总结
使用 synchronized 时,核心是抓住 "锁对象正确、锁粒度合适、避免死锁、利用隐含的可见性 / 有序性保证" 这四大关键点:
- 锁对象必须是多线程共享的引用类型,避免用常量、基本类型、
null。 - 同步块仅包裹临界区,避免过度同步或同步不足。
- 规避死锁,尤其是循环等待和嵌套锁。
- 了解
synchronized的优化机制,避免不必要的性能损耗。
在高并发场景下,若 synchronized 无法满足需求(如超时等待、中断、公平锁),可考虑使用 java.util.concurrent.locks.Lock 接口(如 ReentrantLock),它提供了更灵活的锁控制能力。