Java 什么是线程安全以及如何实现它?

更新于 2025-12-29

Alejandro Ugarte 2024-06-11

1. 概述

Java 原生支持多线程。这意味着 JVM 能够通过在独立的工作线程中并发执行字节码,从而提升应用程序的性能。

尽管多线程是一项强大的功能,但它也伴随着代价。在多线程环境中,我们需要以线程安全的方式编写实现代码。也就是说,多个线程可以同时访问相同的资源,而不会引发错误行为或产生不可预测的结果。这种编程方法被称为“线程安全”。

在本教程中,我们将探讨实现线程安全的不同方法。


2. 无状态实现

在大多数情况下,多线程应用程序中的错误源于多个线程之间不正确地共享状态。

因此,我们首先探讨的方法是使用无状态实现来实现线程安全。

为了更好地理解这种方法,让我们考虑一个简单的工具类,其中包含一个用于计算数字阶乘的静态方法:

public class MathUtils {
    
    public static BigInteger factorial(int number) {
        BigInteger f = new BigInteger("1");
        for (int i = 2; i <= number; i++) {
            f = f.multiply(BigInteger.valueOf(i));
        }
        return f;
    }
}

factorial() 方法是一个无状态的确定性函数:给定特定输入,它总是产生相同的输出。

该方法既不依赖外部状态,也不维护任何内部状态。因此,它被认为是线程安全的,可以被多个线程同时安全调用。

所有线程都可以安全地调用 factorial() 方法,并获得预期结果,彼此之间不会相互干扰,也不会影响该方法为其他线程生成的输出。

因此,无状态实现是实现线程安全最简单的方式


3. 不可变实现

如果我们需要在不同线程之间共享状态,可以通过将类设计为**不可变(immutable)**来创建线程安全的类。

不可变性是一种强大且与语言无关的概念,在 Java 中实现起来也相当容易。

简单来说,当一个类的实例在其构造完成后其内部状态无法再被修改时,该实例就是不可变的。

在 Java 中创建不可变类的最简单方式是:将所有字段声明为 privatefinal,并且不提供 setter 方法:

public class MessageService {
    
    private final String message;

    public MessageService(String message) {
        this.message = message;
    }
    
    // 标准 getter 方法
}

MessageService 对象在构造完成后其状态就无法更改,因此它是有效不可变的,也是线程安全的。

此外,即使 MessageService 实际上是可变的,但只要多个线程仅对其具有只读访问权限,它仍然是线程安全的。

由此可见,不可变性是实现线程安全的另一种方式


4. 线程局部(Thread-Local)字段

在面向对象编程(OOP)中,对象通常需要通过字段维护状态,并通过一个或多个方法实现行为。

如果我们确实需要维护状态,可以通过让字段成为**线程局部(thread-local)**的方式来创建不在线程间共享状态的线程安全类。

我们可以通过在 Thread 子类中定义私有字段,轻松创建字段为线程局部的类。

例如,我们可以定义一个 Thread 子类,它存储一个整数列表:

public class ThreadA extends Thread {
    
    private final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    
    @Override
    public void run() {
        numbers.forEach(System.out::println);
    }
}

与此同时,另一个线程类可能持有字符串列表:

public class ThreadB extends Thread {
    
    private final List<String> letters = Arrays.asList("a", "b", "c", "d", "e", "f");
    
    @Override
    public void run() {
        letters.forEach(System.out::println);
    }
}

在这两个实现中,每个类都拥有自己的状态,但这些状态不会与其他线程共享。因此,这些类是线程安全的。

类似地,我们也可以通过将 ThreadLocal 实例赋值给字段来创建线程局部变量。

考虑以下 StateHolder 类:

public class StateHolder {
    
    private final String state;

    // 标准构造函数 / getter
}

我们可以轻松将其变为线程局部变量:

public class ThreadState {
    
    public static final ThreadLocal<StateHolder> statePerThread = new ThreadLocal<StateHolder>() {
        
        @Override
        protected StateHolder initialValue() {
            return new StateHolder("active");  
        }
    };

    public static StateHolder getState() {
        return statePerThread.get();
    }
}

线程局部字段与普通类字段非常相似,区别在于:每个访问它们的线程都会获得一个独立初始化的副本,从而每个线程都拥有自己的状态。


5. 同步集合(Synchronized Collections)

我们可以使用 Java 集合框架中提供的**同步包装器(synchronization wrappers)**轻松创建线程安全的集合。

例如,我们可以使用其中一个同步包装器来创建线程安全的集合:

Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)));
Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7, 8, 9, 10, 11, 12)));
thread1.start();
thread2.start();

需要注意的是,同步集合在每个方法内部使用了内置锁(intrinsic locking)(我们稍后会讨论内置锁)。

这意味着一次只能有一个线程访问这些方法,其他线程将被阻塞,直到第一个线程释放锁。

因此,由于同步访问的底层逻辑,同步会带来性能开销


6. 并发集合(Concurrent Collections)

除了同步集合,我们还可以使用**并发集合(concurrent collections)**来创建线程安全的集合。

Java 提供了 java.util.concurrent 包,其中包含多种并发集合,例如 ConcurrentHashMap

Map<String, String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("1", "one");
concurrentMap.put("2", "two");
concurrentMap.put("3", "three");

与同步集合不同,并发集合通过将数据划分为多个段来实现线程安全。例如,在 ConcurrentHashMap 中,多个线程可以同时对不同的 map 段加锁,从而允许多个线程同时访问 Map。

由于支持并发线程访问,并发集合的性能远优于同步集合

值得注意的是:同步集合和并发集合仅保证集合本身是线程安全的,并不保证集合中元素的内容是线程安全的


7. 原子对象(Atomic Objects)

我们还可以使用 Java 提供的一组原子类来实现线程安全,包括 AtomicIntegerAtomicLongAtomicBooleanAtomicReference

原子类允许我们在不使用同步的情况下执行原子操作(atomic operations),这些操作是线程安全的。原子操作是指在单个机器级指令中完成的操作。

为了理解这个问题的解决方案,让我们看下面的 Counter 类:

public class Counter {
    
    private int counter = 0;
    
    public void incrementCounter() {
        counter += 1;
    }
    
    public int getCounter() {
        return counter;
    }
}

假设在竞态条件下,两个线程同时访问 incrementCounter() 方法。

理论上,counter 字段的最终值应为 2。但我们无法保证结果,因为两个线程同时执行同一代码块,而 += 操作不是原子的

现在,我们使用 AtomicInteger 对象创建一个线程安全的 Counter 实现:

public class AtomicCounter {
    
    private final AtomicInteger counter = new AtomicInteger();
    
    public void incrementCounter() {
        counter.incrementAndGet();
    }
    
    public int getCounter() {
        return counter.get();
    }
}

这是线程安全的,因为虽然 ++ 操作需要多个步骤,但 incrementAndGet()原子操作


8. 同步方法(Synchronized Methods)

前面的方法非常适合处理集合和基本类型,但有时我们需要更精细的控制。

因此,另一种常见的实现线程安全的方法是使用同步方法

简而言之,一次只能有一个线程访问同步方法,其他线程将被阻塞,直到第一个线程执行完毕或方法抛出异常。

我们可以通过在方法签名前加上 synchronized 关键字,创建同步方法:

public synchronized void incrementCounter() {
    counter += 1;
}

由于一次只能有一个线程访问同步方法,因此线程将依次执行 incrementCounter() 方法,不会发生重叠执行

同步方法依赖于“内置锁(intrinsic locks)”或“监视器锁(monitor locks)”。内置锁是与特定类实例关联的隐式内部实体。

在多线程上下文中,“监视器(monitor)”一词指的是锁在关联对象上所扮演的角色——它强制对一组指定方法或语句的独占访问。

当线程调用同步方法时,它会获取内置锁;方法执行完毕后,它会释放锁,从而允许其他线程获取锁并访问该方法。

我们可以对**实例方法、静态方法和代码块(synchronized statements)**实现同步。


9. 同步代码块(Synchronized Statements)

有时,对整个方法进行同步可能是过度的,如果我们只需要使方法中的一小段代码线程安全。

为了说明这种用例,让我们重构 incrementCounter() 方法:

public void incrementCounter() {
    // 其他不需要同步的操作
    synchronized(this) {
        counter += 1; 
    }
}

这个例子虽然简单,但它展示了如何创建同步代码块。假设该方法现在执行一些额外操作(不需要同步),我们只需将修改状态的相关部分包裹在 synchronized 块中即可。

与同步方法不同,同步代码块必须显式指定提供内置锁的对象,通常是 this 引用。

由于同步是有开销的,这种方式使我们能够仅同步方法中相关的部分

9.1 使用其他对象作为锁

我们可以通过使用另一个对象作为监视器锁(而不是 this)来略微改进 Counter 类的线程安全实现。

这不仅能在多线程环境中协调对共享资源的访问,还能使用外部实体来强制独占访问:

public class ObjectLockCounter {

    private int counter = 0;
    private final Object lock = new Object();
    
    public void incrementCounter() {
        synchronized(lock) {
            counter += 1;
        }
    }
    
    // 标准 getter
}

这里我们使用一个普通的 Object 实例来强制互斥访问。这种实现略优,因为它在锁级别上提升了安全性。

如果使用 this 作为内置锁,攻击者可能通过获取该内置锁并触发拒绝服务(DoS)条件,造成死锁。

相反,使用私有对象作为锁时,该实体对外部不可见,使得攻击者难以获取锁并造成死锁。

9.2 注意事项

尽管我们可以使用任意 Java 对象作为内置锁,但应避免使用字符串作为锁

public class Class1 {
    private static final String LOCK = "Lock";
    // 使用 LOCK 作为内置锁
}

public class Class2 {
    private static final String LOCK = "Lock";
    // 使用 LOCK 作为内置锁
}

乍看之下,这两个类似乎使用了两个不同的对象作为锁。但由于**字符串驻留(string interning)**机制,这两个 "Lock" 值实际上可能指向字符串池中的同一个对象。也就是说,Class1Class2 共享了同一个锁

这在并发上下文中可能导致意外行为。

除了字符串,我们还应避免使用可缓存或可复用的对象作为内置锁。例如,Integer.valueOf() 方法会缓存小整数。因此,即使在不同类中调用 Integer.valueOf(1),也会返回同一个对象。


10. volatile 字段

同步方法和代码块有助于解决线程间的变量可见性问题。即便如此,普通类字段的值仍可能被 CPU 缓存。因此,即使更新是同步的,其他线程也可能看不到后续的更新。

为避免这种情况,我们可以使用 volatile 类字段:

public class Counter {
    private volatile int counter;
    // 标准构造函数 / getter
}

使用 volatile 关键字后,我们指示 JVM 和编译器将 counter 变量存储在主内存中。这样可以确保每次 JVM 读取 counter 时,都是从主内存读取,而不是 CPU 缓存;同样,每次写入也会直接写入主内存。

此外,使用 volatile 变量还能确保:对该线程可见的所有变量都会从主内存中读取

考虑以下示例:

public class User {
    private String name;
    private volatile int age;
    // 标准构造函数 / getter
}

在这种情况下,每当 JVM 将 age(volatile 变量)写入主内存时,也会将非 volatile 的 name 变量写入主内存。这确保了两个变量的最新值都存储在主内存中,因此后续更新对其他线程自动可见。

同样,如果一个线程读取了 volatile 变量的值,那么该线程可见的所有变量也会从主内存中读取。

这种由 volatile 变量提供的扩展保证被称为 “完整的 volatile 可见性保证(full volatile visibility guarantee)”


11. 可重入锁(Reentrant Locks)

Java 提供了一组改进版的 Lock 实现,其行为比上述内置锁更为灵活。

使用内置锁时,锁的获取模型较为僵化:一个线程获取锁 → 执行方法或代码块 → 释放锁 → 其他线程才能获取。

该模型没有机制检查等待队列中的线程,也无法优先授予等待时间最长的线程

ReentrantLock 实例正好可以做到这一点,从而防止排队线程出现某些类型的资源饥饿:

public class ReentrantLockCounter {
    private int counter;
    private final ReentrantLock reLock = new ReentrantLock(true);
    
    public void incrementCounter() {
        reLock.lock();
        try {
            counter += 1;
        } finally {
            reLock.unlock();
        }
    }
    
    // 标准构造函数 / getter
}

ReentrantLock 构造函数接受一个可选的 fairness(公平性)布尔参数。当设为 true 时,如果有多个线程尝试获取锁,JVM 会优先授予等待时间最长的线程。


12. 读写锁(Read/Write Locks)

另一种强大的线程安全机制是使用 ReadWriteLock 实现。

ReadWriteLock 实际上使用一对关联的锁:一个用于只读操作,另一个用于写操作。

因此,只要没有线程在写入资源,就可以允许多个线程同时读取该资源。而一旦有线程开始写入,就会阻止其他线程读取。

以下是使用 ReadWriteLock 的示例:

public class ReentrantReadWriteLockCounter {
    
    private int counter;
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    
    public void incrementCounter() {
        writeLock.lock();
        try {
            counter += 1;
        } finally {
            writeLock.unlock();
        }
    }
    
    public int getCounter() {
        readLock.lock();
        try {
            return counter;
        } finally {
            readLock.unlock();
        }
    }

    // 标准构造函数
}

13. 结论

在本文中,我们学习了 Java 中的线程安全概念,并深入探讨了实现线程安全的各种方法:

  • 无状态实现
  • 不可变对象
  • 线程局部变量
  • 同步集合与并发集合
  • 原子类
  • 同步方法与同步代码块
  • volatile 字段
  • 可重入锁与读写锁

根据具体场景选择合适的方法,可以有效提升程序的并发性能与可靠性。