1. 概述

在 Java 中,同步机制 是解决多线程并发问题的重要手段。但如果使用不当,它也可能成为程序的性能瓶颈甚至引发死锁等严重问题。

本文将带你盘点几个常见的同步使用误区,并提供更合理的替代方案。目标是帮助你避开这些坑,写出更健壮、高效的并发代码。

2. 同步的基本原则

核心原则:只对那些你能确保不会被外部代码锁定的对象进行同步。

换句话说:

不要使用可复用或共享的对象作为锁对象

因为这些对象可能被 JVM 中的其他代码访问甚至修改,一旦外部代码也对其加锁,就可能导致死锁或者不可预测的行为。

接下来我们结合一些具体类型来说明这个问题,比如 StringBooleanIntegerObject

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 类型只有两个值:truefalse,这两个值在 JVM 中也是共享的单例对象。

❌ 因此,使用 Boolean 对象作为锁是非常危险的:

private final Boolean booleanLock = Boolean.FALSE;
public void booleanBadPractice() {
    synchronized (booleanLock) {
        // ...
    }
}

⚠️ 如果外部代码也使用 Boolean.FALSEBoolean.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(); 作为同步锁对象。

源码地址:GitHub - core-java-concurrency-advanced-4


原始标题:Bad Practices With Synchronization