源本科技 | 码上会

Java 线程安全

2026/01/29
39
0

学习目标

  • 理解线程安全的基本概念及其在并发编程中的重要性

  • 掌握 Java 中实现线程安全的四种主要方法

  • 能够区分线程安全类与非线程安全类的使用场景和性能差异

  • 学会通过代码实践验证线程安全机制的效果


什么是线程安全

线程安全(Thread Safety)是指当多个线程同时访问同一个对象或执行同一段代码时,程序仍能保持正确的行为,不会出现数据损坏或不可预期的结果。一个类或方法如果在线程并发访问下依然能保证数据一致性,则被认为是线程安全的。无论线程调度顺序如何变化,共享数据的状态始终是正确的。


实现线程安全的四种方式

1. 使用同步机制

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 必须等待其执行完毕并释放锁后才能进入,从而保证了操作的原子性和可见性。


2. 使用 volatile 关键字

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

说明:
由于 ab 被声明为 volatilet2 总能读取到 t1 写入的最新值。但注意:a++ 本身不是原子操作(包含读 - 改 - 写),因此在高并发下仍可能出现不一致(本例因循环次数少且执行快,结果看似正确)。


3. 使用原子变量

java.util.concurrent.atomic 包提供了如 AtomicIntegerAtomicLong 等类,它们利用 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
    }
}

说明:
AtomicIntegerincrementAndGet() 使用底层 CPU 指令实现原子递增,无需显式加锁,性能优于 synchronized,特别适合高并发计数场景。


4. 使用不可变对象

不可变对象一旦创建,其状态就无法被修改。由于没有可变状态,天然具备线程安全性。

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 实例,无需额外同步。


线程安全 vs 非线程安全

特性

线程安全

非线程安全

定义

支持多线程并发访问而不破坏数据一致性

未设计用于并发访问,需外部同步

同步机制

内部使用锁或原子操作

无内置同步,依赖调用者管理

适用场景

多线程环境(如 Web 服务器、并发任务)

单线程或已受控的并发上下文

性能

因锁或 CAS 开销,通常较慢

无同步开销,性能更高

可扩展性

高并发下可能因锁竞争成为瓶颈

在单线程或低并发下扩展性好

典型类

Vector, Hashtable, ConcurrentHashMap, StringBuffer

ArrayList, HashMap, StringBuilder, SimpleDateFormat

提示:
SimpleDateFormat 是经典反面教材——它不是线程安全的,若在多线程中共享使用会导致日期解析错误。推荐使用 java.time 包中的 DateTimeFormatter(不可变且线程安全)。


重点总结

  • 线程安全的核心是保证共享数据在并发访问下的一致性与正确性

  • 四种主要实现方式各有适用场景:

    • synchronized:简单直接,适合保护临界区;

    • volatile:仅解决可见性问题,不适用于复合操作;

    • 原子类:高性能无锁方案,适合计数器等简单状态更新;

    • 不可变对象:最安全的设计模式,应优先考虑。

  • 并非所有场景都需要线程安全——过度同步会损害性能,应根据实际并发需求选择合适策略。


思考题

  1. 为什么 volatile 不能保证 a++ 操作的线程安全?请结合 JVM 内存模型解释。

  2. 在什么情况下你会选择 AtomicInteger 而不是 synchronized?性能和功能上各有什么权衡?

  3. 设计一个线程安全的缓存类,要求支持 put(key, value)get(key) 操作,并说明你采用的线程安全策略及其理由。