第2章:线程安全性
一、导读
2.1 本章学习目标
本章讲解线程安全性的概念,以及如何编写线程安全的代码。线程安全是并发编程的核心概念,理解它对于编写正确的并发程序至关重要。
本章的核心学习目标包括:
- 理解什么是线程安全:多个线程访问时仍能保持正确性
- 掌握竞态条件:多个线程的执行顺序影响结果
- 理解原子性、可见性、有序性:并发编程的三大特性
- 掌握线程安全的实现策略:不可变、线程封闭、加锁
2.2 为什么这一章如此重要
线程安全是并发编程的基础。如果程序不是线程安全的,就可能出现数据不一致、计算错误等问题。理解线程安全的概念,对于:
- 编写正确的并发程序:避免数据竞争
- 设计线程安全的类:保证类的正确性
- 排查并发问题:定位线程安全问题
二、核心概念详解
2.1 什么是线程安全
当多个线程访问某个类时,不管运行时环境采用何种调度方式或线程间的交互次序,都可以再主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
// 线程安全的类
public class ThreadSafeCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}2.2 竞态条件
竞态条件是指当多个线程访问共享资源时,程序的执行结果依赖于线程的执行顺序。
// 竞态条件示例
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 非原子操作:读取、加1、写入
}
public int getCount() {
return count;
}
}
// 问题:多个线程同时调用 increment(),结果可能不正确2.3 原子性
原子性是指操作不可被中断,要么全部执行,要么全部不执行。
// 原子操作
int x = 10; // 赋值是原子的
// 非原子操作
x++; // 包含三个操作:读取 x、加1、写入 x
// 使用 synchronized 保证原子性
public synchronized void increment() {
count++;
}
// 使用 AtomicInteger 保证原子性
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();2.4 可见性
可见性是指一个线程的修改对其他线程是否可见。
// 可见性问题示例
public class VisibilityProblem {
private boolean ready = false;
public void writer() {
ready = true; // 修改可能不会立即对其他线程可见
}
public void reader() {
if (ready) { // 可能看不到最新的值
// ...
}
}
}
// 使用 volatile 保证可见性
public class VisibilitySolution {
private volatile boolean ready = false;
public void writer() {
ready = true; // 保证立即对其他线程可见
}
public void reader() {
if (ready) { // 保证能看到最新的值
// ...
}
}
}2.5 有序性
有序性是指指令执行的顺序。由于编译器和处理器的优化,指令的执行顺序可能与代码顺序不同。
// 指令重排序示例
public class Reordering {
private int a = 0;
private int b = 0;
public void write() {
a = 1; // 可能被重排序
b = 2; // 可能被重排序
}
public void read() {
if (b == 2) {
// a 可能还是 0,因为指令重排序
}
}
}
// 使用 volatile 禁止指令重排序
public class NoReordering {
private volatile int a = 0;
private volatile int b = 0;
public void write() {
a = 1; // 不会被重排序到 b = 2 之后
b = 2; // 不会被重排序到 a = 1 之前
}
}2.6 线程安全的实现策略
2.6.1 不可变
不可变对象一定是线程安全的。
// 不可变类
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}2.6.2 线程封闭
将对象封装在单个线程中,避免共享。
// 使用 ThreadLocal 实现线程封闭
public class ThreadLocalExample {
private ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String formatDate(Date date) {
return dateFormat.get().format(date);
}
}2.6.3 加锁
使用锁保护共享状态。
// 使用 synchronized
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
}
// 使用 ReentrantLock
public class LockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
}三、重要知识点
3.1 内置锁
Java 中的每个对象都可以作为锁,这种锁称为内置锁。
// 同步方法
public synchronized void method() {
// 使用 this 作为锁
}
// 同步代码块
public void method() {
synchronized (this) {
// 使用 this 作为锁
}
}
// 使用其他对象作为锁
private final Object lock = new Object();
public void method() {
synchronized (lock) {
// 使用 lock 作为锁
}
}3.2 重入
当线程请求一个由其他线程持有的锁时,该线程会阻塞。但当线程请求一个由自己持有的锁时,如果该锁是可重入的,请求就会成功。
// 重入示例
public class Reentrant {
public synchronized void method1() {
method2(); // 可以再次获取锁
}
public synchronized void method2() {
// 可以获取锁
}
}3.3 复合操作
复合操作是指由多个操作组成的操作序列,需要保证原子性。
// 复合操作示例
public class CompoundOperation {
private int count = 0;
// 非原子操作
public void increment() {
count++; // 读取、加1、写入
}
// 原子操作
public synchronized void safeIncrement() {
count++;
}
}四、常见误区与难点
4.1 误区一:"基本类型的操作都是原子的"
基本类型的操作不一定是原子的。例如,long 和 double 的读写操作在某些 JVM 实现中不是原子的。
4.2 误区二:"synchronized 只保护代码块"
synchronized 保护的是锁对象的状态,而不仅仅是代码块。
4.3 难点:理解 happens-before 关系
happens-before 是 Java 内存模型的核心概念,它定义了操作之间的可见性保证。
4.4 难点:设计线程安全的类
设计线程安全的类需要考虑:
- 哪些状态是共享的
- 哪些操作是原子的
- 使用什么锁策略
五、实践应用
5.1 使用 JMH 测试线程安全性
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Group)
public class ConcurrencyTest {
private Counter counter;
@Setup
public void setup() {
counter = new Counter();
}
@Benchmark
@Group("t1")
public void increment() {
counter.increment();
}
@Benchmark
@Group("t1")
public int getCount() {
return counter.getCount();
}
}5.2 使用 FindBugs 检测线程安全问题
# 使用 FindBugs 检测线程安全问题
findbugs -include filter.xml -effort:max target/classes5.3 使用 ThreadSanitizer 检测数据竞争
# 使用 ThreadSanitizer 检测数据竞争
java -agentpath:/path/to/tsan/libtsan.so MyProgram六、本章小结
第2章讲解了线程安全性的核心概念:
线程安全:多个线程访问时仍能保持正确性
竞态条件:多个线程的执行顺序影响结果
原子性:操作不可被中断
可见性:一个线程的修改对其他线程可见
有序性:指令执行的顺序
线程安全的实现策略:不可变、线程封闭、加锁
后续章节将讲解如何安全地共享对象。