理解线程安全的基本概念及其在并发编程中的重要性
掌握 Java 中实现线程安全的四种主要方法
能够区分线程安全类与非线程安全类的使用场景和性能差异
学会通过代码实践验证线程安全机制的效果
线程安全(Thread Safety)是指当多个线程同时访问同一个对象或执行同一段代码时,程序仍能保持正确的行为,不会出现数据损坏或不可预期的结果。一个类或方法如果在线程并发访问下依然能保证数据一致性,则被认为是线程安全的。无论线程调度顺序如何变化,共享数据的状态始终是正确的。
Java 提供 synchronized 关键字,用于确保同一时刻只有一个线程可以执行某个方法或代码块,从而避免竞态条件。
class Calculator {
synchronized void sum(int n) {
Thread current = Thread.currentThread();
for (int i = 1; i <= 5; i++) {
System.out.println(current.getName() + " : " + (n + i));
}
}
}
class Worker extends Thread {
Calculator calc = new Calculator();
public void run() {
calc.sum(10);
}
}
public class MainApp {
public static void main(String[] args) {
Worker worker = new Worker();
Thread t1 = new Thread(worker);
Thread t2 = new Thread(worker);
t1.setName("Thread A");
t2.setName("Thread B");
t1.start();
t2.start();
}
}输出示例:
Thread A : 11
Thread A : 12
Thread A : 13
Thread A : 14
Thread A : 15
Thread B : 11
Thread B : 12
Thread B : 13
Thread B : 14
Thread B : 15说明:synchronized 方法会对当前对象加锁。当 Thread A 进入 sum() 方法时,Thread B 必须等待其执行完毕并释放锁后才能进入,从而保证了操作的原子性和可见性。
volatile 关键字确保变量的修改对所有线程立即可见。它强制线程每次读取变量时都从主内存中获取最新值,而不是使用本地缓存。
注意:
volatile不保证原子性,仅适用于“单次读 / 写”操作的可见性保障。
public class VolatileDemo {
static volatile int a = 0, b = 0;
static void incrementBoth() {
a++;
b++;
}
static void printValues() {
System.out.println("a=" + a + " b=" + b);
}
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
incrementBoth();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
printValues();
}
});
t1.start();
t2.start();
}
}可能的输出:
a=5 b=5
a=5 b=5
a=5 b=5
a=5 b=5
a=5 b=5说明:
由于 a 和 b 被声明为 volatile,t2 总能读取到 t1 写入的最新值。但注意:a++ 本身不是原子操作(包含读 - 改 - 写),因此在高并发下仍可能出现不一致(本例因循环次数少且执行快,结果看似正确)。
java.util.concurrent.atomic 包提供了如 AtomicInteger、AtomicLong 等类,它们利用 CAS(Compare-And-Swap)机制实现无锁的线程安全操作。
import java.util.concurrent.atomic.AtomicInteger;
class SafeCounter {
AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作
}
}
public class CounterTest {
public static void main(String[] args) throws Exception {
SafeCounter counter = new SafeCounter();
Thread t1 = new Thread(() -> {
for (int i = 1; i <= 2000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 1; i <= 2000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count); // 输出:4000
}
}说明:AtomicInteger 的 incrementAndGet() 使用底层 CPU 指令实现原子递增,无需显式加锁,性能优于 synchronized,特别适合高并发计数场景。
不可变对象一旦创建,其状态就无法被修改。由于没有可变状态,天然具备线程安全性。
final class Student {
private final String name;
private final int rollNo;
public Student(String name, int rollNo) {
this.name = name;
this.rollNo = rollNo;
}
public String getName() {
return name;
}
public int getRollNo() {
return rollNo;
}
}设计要点:
类声明为 final,防止被继承篡改行为
所有字段为 private final,初始化后不可更改
不提供任何 setter 方法或可修改内部状态的方法
多个线程可以安全地共享同一个 Student 实例,无需额外同步。
提示:
SimpleDateFormat是经典反面教材——它不是线程安全的,若在多线程中共享使用会导致日期解析错误。推荐使用java.time包中的DateTimeFormatter(不可变且线程安全)。
线程安全的核心是保证共享数据在并发访问下的一致性与正确性。
四种主要实现方式各有适用场景:
synchronized:简单直接,适合保护临界区;
volatile:仅解决可见性问题,不适用于复合操作;
原子类:高性能无锁方案,适合计数器等简单状态更新;
不可变对象:最安全的设计模式,应优先考虑。
并非所有场景都需要线程安全——过度同步会损害性能,应根据实际并发需求选择合适策略。
为什么 volatile 不能保证 a++ 操作的线程安全?请结合 JVM 内存模型解释。
在什么情况下你会选择 AtomicInteger 而不是 synchronized?性能和功能上各有什么权衡?
设计一个线程安全的缓存类,要求支持 put(key, value) 和 get(key) 操作,并说明你采用的线程安全策略及其理由。