2Java 并发编程实战

线程安全性

并发编程的核心概念

线程安全竞态条件原子性可见性有序性

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第2章:线程安全性

一、导读

2.1 本章学习目标

本章讲解线程安全性的概念,以及如何编写线程安全的代码。线程安全是并发编程的核心概念,理解它对于编写正确的并发程序至关重要。

本章的核心学习目标包括:

  • 理解什么是线程安全:多个线程访问时仍能保持正确性
  • 掌握竞态条件:多个线程的执行顺序影响结果
  • 理解原子性、可见性、有序性:并发编程的三大特性
  • 掌握线程安全的实现策略:不可变、线程封闭、加锁

2.2 为什么这一章如此重要

线程安全是并发编程的基础。如果程序不是线程安全的,就可能出现数据不一致、计算错误等问题。理解线程安全的概念,对于:

  • 编写正确的并发程序:避免数据竞争
  • 设计线程安全的类:保证类的正确性
  • 排查并发问题:定位线程安全问题

二、核心概念详解

2.1 什么是线程安全

当多个线程访问某个类时,不管运行时环境采用何种调度方式或线程间的交互次序,都可以再主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

java
// 线程安全的类
public class ThreadSafeCounter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

2.2 竞态条件

竞态条件是指当多个线程访问共享资源时,程序的执行结果依赖于线程的执行顺序。

java
// 竞态条件示例
public class UnsafeCounter {
    private int count = 0;
    
    public void increment() {
        count++;  // 非原子操作:读取、加1、写入
    }
    
    public int getCount() {
        return count;
    }
}

// 问题:多个线程同时调用 increment(),结果可能不正确

2.3 原子性

原子性是指操作不可被中断,要么全部执行,要么全部不执行。

java
// 原子操作
int x = 10;  // 赋值是原子的

// 非原子操作
x++;  // 包含三个操作:读取 x、加1、写入 x

// 使用 synchronized 保证原子性
public synchronized void increment() {
    count++;
}

// 使用 AtomicInteger 保证原子性
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

2.4 可见性

可见性是指一个线程的修改对其他线程是否可见。

java
// 可见性问题示例
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 有序性

有序性是指指令执行的顺序。由于编译器和处理器的优化,指令的执行顺序可能与代码顺序不同。

java
// 指令重排序示例
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 不可变

不可变对象一定是线程安全的。

java
// 不可变类
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 线程封闭

将对象封装在单个线程中,避免共享。

java
// 使用 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 加锁

使用锁保护共享状态。

java
// 使用 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 中的每个对象都可以作为锁,这种锁称为内置锁。

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 重入

当线程请求一个由其他线程持有的锁时,该线程会阻塞。但当线程请求一个由自己持有的锁时,如果该锁是可重入的,请求就会成功。

java
// 重入示例
public class Reentrant {
    public synchronized void method1() {
        method2();  // 可以再次获取锁
    }
    
    public synchronized void method2() {
        // 可以获取锁
    }
}

3.3 复合操作

复合操作是指由多个操作组成的操作序列,需要保证原子性。

java
// 复合操作示例
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 测试线程安全性

java
@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 检测线程安全问题

bash
# 使用 FindBugs 检测线程安全问题
findbugs -include filter.xml -effort:max target/classes

5.3 使用 ThreadSanitizer 检测数据竞争

bash
# 使用 ThreadSanitizer 检测数据竞争
java -agentpath:/path/to/tsan/libtsan.so MyProgram

六、本章小结

第2章讲解了线程安全性的核心概念:

线程安全:多个线程访问时仍能保持正确性

竞态条件:多个线程的执行顺序影响结果

原子性:操作不可被中断

可见性:一个线程的修改对其他线程可见

有序性:指令执行的顺序

线程安全的实现策略:不可变、线程封闭、加锁

后续章节将讲解如何安全地共享对象。


2/15线程安全
0%