1. 概述
在 Java 中,同步机制 是解决多线程并发问题的重要手段。但如果使用不当,它也可能成为程序的性能瓶颈甚至引发死锁等严重问题。
本文将带你盘点几个常见的同步使用误区,并提供更合理的替代方案。目标是帮助你避开这些坑,写出更健壮、高效的并发代码。
2. 同步的基本原则
✅ 核心原则:只对那些你能确保不会被外部代码锁定的对象进行同步。
换句话说:
❌ 不要使用可复用或共享的对象作为锁对象。
因为这些对象可能被 JVM 中的其他代码访问甚至修改,一旦外部代码也对其加锁,就可能导致死锁或者不可预测的行为。
接下来我们结合一些具体类型来说明这个问题,比如 String、Boolean、Integer 和 Object。
3. String 字面量
3.1. 错误做法
Java 中的字符串字面量会被放入字符串常量池中并被重复使用,所以用它们来做同步锁是非常危险的:
public void stringBadPractice1() {
String stringLock = "LOCK_STRING";
synchronized (stringLock) {
// ...
}
}
即使你把它声明为 private final
,它依然是从常量池中获取的引用:
private final String stringLock = "LOCK_STRING";
public void stringBadPractice2() {
synchronized (stringLock) {
// ...
}
}
甚至连调用 .intern()
方法也不行:
private final String internedStringLock = new String("LOCK_STRING").intern();
public void stringBadPractice3() {
synchronized (internedStringLock) {
// ...
}
}
⚠️ 根据 Java 文档,intern()
方法会返回字符串池中的唯一实例,如果不存在则添加进去。所以它本质上还是在复用对象。
📌 注意:所有的字符串字面量和字符串常量表达式都会被自动 intern。
3.2. 正确做法
✅ 使用 new String(...)
创建新的实例,这样可以保证每次都是一个新的对象引用,避免与其他地方冲突。
我们来修复上面的问题:
private final String stringLock = new String("LOCK_STRING");
public void stringSolution() {
synchronized (stringLock) {
// ...
}
}
这样每个实例都有自己独立的内置锁(intrinsic lock),并且我们通过 private final
确保外部无法访问这个锁对象。
4. Boolean 字面量
Boolean 类型只有两个值:true
和 false
,这两个值在 JVM 中也是共享的单例对象。
❌ 因此,使用 Boolean 对象作为锁是非常危险的:
private final Boolean booleanLock = Boolean.FALSE;
public void booleanBadPractice() {
synchronized (booleanLock) {
// ...
}
}
⚠️ 如果外部代码也使用 Boolean.FALSE
或 Boolean.TRUE
加锁,就可能造成死锁或阻塞。
✅ 所以:永远不要使用 Boolean 对象作为同步锁。
5. 装箱基本类型
5.1. 错误做法
与 Boolean 类似,一些装箱类型(如 Integer)也会复用实例,尤其是在 -128 到 127
之间的值会被缓存。
来看一个错误示例:
private int count = 0;
private final Integer intLock = count;
public void boxedPrimitiveBadPractice() {
synchronized (intLock) {
count++;
// ...
}
}
如果 count
是 100,那么这个 Integer
实例可能是从缓存中获取的,容易与其他代码冲突。
5.2. 正确做法
✅ 解决方法同样是使用 new
关键字创建一个新的实例:
private int count = 0;
private final Integer intLock = new Integer(count);
public void boxedPrimitiveSolution() {
synchronized (intLock) {
count++;
// ...
}
}
虽然这种方式看起来有些“丑”,但它是安全的。
6. 类级别的同步
当使用 synchronized
关键字修饰方法或使用 synchronized(this)
时,JVM 会使用该对象自身作为监视器锁(monitor lock)。这在某些情况下会导致安全隐患。
6.1. 错误做法
来看一个类定义:
public class Animal {
private String name;
private String owner;
// getters and constructors
public synchronized void setName(String name) {
this.name = name;
}
public void setOwner(String owner) {
synchronized (this) {
this.owner = owner;
}
}
}
如果外部代码获取了这个对象的引用并对其加锁:
Animal animalObj = new Animal("Tommy", "John");
synchronized (animalObj) {
while(true) {
Thread.sleep(Integer.MAX_VALUE);
}
}
⚠️ 这样就会导致 setName()
和 setOwner()
方法永远无法获取锁,引发死锁或长时间阻塞。
6.2. 正确做法
✅ 推荐使用私有的锁对象(private lock object)来替代对象自身的锁。
你可以定义一个 private final Object
作为锁对象,这样外部代码无法访问:
public class Animal {
// ...
private final Object objLock1 = new Object();
private final Object objLock2 = new Object();
public void setName(String name) {
synchronized (objLock1) {
this.name = name;
}
}
public void setOwner(String owner) {
synchronized (objLock2) {
this.owner = owner;
}
}
}
✅ 优点:
- 更高的安全性:外部无法访问锁对象
- 更好的并发性:可以为不同方法使用不同的锁对象,实现更细粒度的同步
- 更灵活:使用代码块同步而不是方法同步,避免不必要的同步范围
📌 如果方法中涉及静态变量的修改,则应使用静态锁对象:
private static int staticCount = 0;
private static final Object staticObjLock = new Object();
public void staticVariableSolution() {
synchronized (staticObjLock) {
count++;
// ...
}
}
7. 总结
本文总结了 Java 同步编程中的几个常见误区:
类型 | 建议 | 原因 |
---|---|---|
String | ❌ 不要用字面量 | 会被复用,容易冲突 |
Boolean | ❌ 不要用 Boolean 对象 | true/false 是单例 |
Integer 等 | ❌ 不要用装箱类型 | 小整数会被缓存 |
this / 类 | ❌ 不要直接同步对象自身 | 外部可访问,容易造成死锁 |
Object | ✅ 使用 private final 锁 | 安全、可控、可并发 |
📌 最佳实践:始终使用 private final Object lock = new Object();
作为同步锁对象。