线程安全完全指南:从 Java 到 Kotlin,一文吃透并发编程

线程安全完全指南:从 Java 到 Kotlin,一文吃透并发编程

多线程环境下,一行看似无害的 count++ 就可能导致结果出错。

这篇文章从「问题根源」出发,系统梳理 Java 和 Kotlin 中的线程安全方案,帮你建立完整的知识体系,而不是零散地记 API。


一、从一个出错的计数器说起

先看一段看似人畜无害的代码:

java 复制代码
public class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread[] threads = new Thread[1000];
        for (int i = 0; i < 1000; i++) {
            threads[i] = new Thread(counter::increment);
            threads[i].start();
        }
        for (Thread t : threads) t.join();
        System.out.println(counter.getCount()); // 期望 1000,实际可能是 998、987......
    }
}

count++ 只有一行代码,为什么会出错?因为它不是原子操作,JVM 层面实际上是三步:

markdown 复制代码
1. 读取 count 的值(read)
2. 将值加 1(modify)
3. 写回 count(write)

两个线程交错执行时,就会出现「丢失更新」。

线程不安全的三宗罪

所有的并发问题,本质上都可以归结为三个特性被破坏:

特性 含义 经典场景
原子性 操作不可中断,要么全做要么全不做 count++、i = a + b
可见性 一个线程修改后,其他线程能立即看到 主内存缓存导致读到旧值
有序性 指令不按代码顺序执行(编译器/CPU 重排序) 单例 DCL 中对象半初始化

Java 内存模型(JMM)通过 happens-before 原则来约束这三个特性,而我们接下来的所有工具,都是在不同程度上提供这三个保证。


二、Java 线程安全工具箱

2.1 synchronized ------ 内置锁,万金油

synchronized 是 Java 最基础的同步手段,它能同时保证原子性、可见性、有序性:

java 复制代码
public class SafeCounter {
    private int count = 0;

    public synchronized void increment() { // 实例锁
        count++;
    }

    public static synchronized void staticMethod() { // 类锁(Class 对象)
        // ...
    }

    public void blockLock() {
        synchronized (this) { // 代码块锁,粒度更细
            count++;
        }
    }
}

锁对象选择:

  • 实例方法锁 → this
  • 静态方法锁 → Xxx.class
  • 代码块锁 → 自定义对象(推荐,减少锁范围)

JDK 6 之后 synchronized 经过大量优化(偏向锁 → 轻量级锁 → 重量级锁的升级路径),性能已经不再是「能不用就不用」的存在。简单场景优先用它,可读性最好。

2.2 volatile ------ 轻量级的可见性保证

volatile 只保证可见性 和有序性,不保证原子性:

java 复制代码
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                     // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) {             // 第二次检查
                    instance = new Singleton();     // 如果没有 volatile,
                }                                   // 这里可能被重排序为:
            }                                       // 1.分配内存 2.引用指向 3.初始化
        }                                           // 其他线程可能拿到未初始化的对象!
        return instance;
    }
}

这是经典的 DCL(Double-Checked Locking)单例 。volatile 在这里的作用是禁止 new Singleton() 的指令重排序。

⚠️ 误区 :volatile int count; count++ 依然不是线程安全的!++ 不是原子操作,volatile 无能为力。

2.3 ReentrantLock ------ 功能更强的显式锁

相比 synchronized,ReentrantLock 提供了更灵活的能力:

java 复制代码
public class LockDemo {
    private final ReentrantLock lock = new ReentrantLock(true); // 公平锁
    private int count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock(); // 必须放在 finally 中!
        }
    }

    public void tryIncrement() {
        if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { // 可超时,避免死锁
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
    }

    public void interruptibleIncrement() throws InterruptedException {
        lock.lockInterruptibly(); // 可中断等待
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}
对比项 synchronized ReentrantLock
获取锁 自动获取/释放 手动 lock/unlock
可中断 ❌ ✅
超时获取 ❌ ✅
公平锁 ❌(非公平) ✅ 可选
条件变量 一个 wait/notify 多个 Condition

实践建议 :能用 synchronized 就用它,需要超时、中断、公平性时再用 ReentrantLock。

2.4 CAS 与 Atomic 家族 ------ 无锁编程

CAS(Compare-And-Swap)是一种乐观锁策略:先比较,一致才更新,失败就重试。

java 复制代码
public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // 内部是 CAS 自旋,无锁、无阻塞
    }
}

高并发下性能更优的还有 LongAdder(JDK 8+),它把热点值分散到多个 Cell 中,降低 CAS 竞争:

java 复制代码
LongAdder adder = new LongAdder();
adder.increment();
long sum = adder.sum(); // 汇总所有 Cell

选择建议:

  • 低并发:AtomicInteger 足够
  • 高并发计数(统计、监控):LongAdder 性能碾压
  • 需要复合操作(先读后改):CAS 的 updateAndGet() 或回到锁

2.5 ThreadLocal ------ 线程隔离

有些场景不需要共享,每个线程各持有一份即可:

java 复制代码
public class ThreadLocalDemo {
    private static final ThreadLocal<SimpleDateFormat> FORMATTER =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    public String format(Date date) {
        return FORMATTER.get().format(date); // 每个线程用自己的,天然安全
    }
}

⚠️ 内存泄漏坑 :线程池场景下线程会复用,ThreadLocal 用完必须 remove(),否则旧值一直残留,严重时 OOM:

java 复制代码
try {
    FORMATTER.get().format(date);
} finally {
    FORMATTER.remove(); // 好习惯
}

2.6 并发容器 ------ 开箱即用

容器 替代 特点
ConcurrentHashMap Hashtable / 同步 Map 分段锁(JDK7)→ CAS + synchronized(JDK8)
CopyOnWriteArrayList 同步 List 写时复制,读多写少神器
BlockingQueue 手动 wait/notify 生产者-消费者模型标配
ConcurrentLinkedQueue 同步 Queue 无锁队列

⚠️ 注意:ConcurrentHashMap 保证的是单个方法的原子性,复合操作仍需额外同步:

java 复制代码
// 错误:check-then-act 不是原子的
if (!map.containsKey(key)) {
    map.put(key, value); // 两个线程可能同时进入这里
}

// 正确:用原子方法
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> newValue());

2.7 不可变对象 ------ 终极方案

最好的线程安全,是根本不需要线程安全。不可变对象创建后状态不再改变,可以随意共享、随意跨线程传递:

java 复制代码
public final class User {  // 类不可继承
    private final String name;   // 字段 final,创建后不可改
    private final int age;
    private final List<String> tags;

    public User(String name, int age, List<String> tags) {
        this.name = name;
        this.age = age;
        // 防御性拷贝,防止外部引用改动内部状态
        this.tags = Collections.unmodifiableList(new ArrayList<>(tags));
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public List<String> getTags() { return tags; }
}

Java 中常见的不可变实现:

  • final 字段:保证引用不会被重新赋值
  • String:本身就是不可变的
  • Collections.unmodifiableXXX():包装成不可变视图
  • JDK 16+ record:一行代码声明不可变数据类
java 复制代码
record Point(int x, int y) {} // 不可变,可直接多线程共享,无需任何同步

⚠️ 注意 :final 只保证引用的不可变,对象内部状态仍可能可变(比如上面的 List 字段),需要配合防御性拷贝。

2.8 JDK 21 虚拟线程(彩蛋)

java 复制代码
Thread.startVirtualThread(() -> {
    // 阻塞式写法,性能接近协程
});

虚拟线程让「一个请求一个线程」的模型重新变得可行,但它不改变线程安全的语义------共享可变状态依然需要同步。


三、Kotlin 线程安全方案

3.1 先明确:Kotlin 完全兼容 Java 方案

Kotlin 运行在 JVM 上,上面所有工具都能直接用,只是写法更简洁:

kotlin 复制代码
class SafeCounter {
    private var count = 0

    @Synchronized // 等价于 Java 的 synchronized 方法
    fun increment() {
        count++
    }

    @Volatile
    private var flag = false // 等价于 volatile 字段

    fun withLock() {
        synchronized(this) { // 注意:Kotlin 中 synchronized 是函数
            count++
        }
    }
}

单例更是简单到令人发指:

kotlin 复制代码
object Singleton { // 天然线程安全的单例,JVM 保证类加载线程安全
    fun doSomething() {}
}

3.2 协程:换一条赛道

Kotlin 协程的最大优势是:用挂起代替阻塞,用结构化并发代替手动线程管理。很多传统并发问题,在协程里压根不会出现。

单线程调度器规避竞争
kotlin 复制代码
val singleThreadDispatcher = Dispatchers.Default.limitedParallelism(1)

scope.launch(singleThreadDispatcher) {
    // 这里面的代码天然串行执行,不需要任何锁
    count++
    updateUI(count)
}
Mutex ------ 协程世界的锁
kotlin 复制代码
class CoroutineCounter {
    private var count = 0
    private val mutex = Mutex()

    suspend fun increment() {
        mutex.withLock { // 挂起等待,不阻塞线程!
            count++
        }
    }
}

⚠️ 协程中使用普通锁的坑:

kotlin 复制代码
val lock = ReentrantLock()

suspend fun bad() {
    lock.lock()        // ❌ 阻塞了整个线程!
    delay(100)         // 协程挂起,但线程还被锁占着
    lock.unlock()      // 其他协程全卡死了
}

suspend fun good() {
    mutex.lock()       // ✅ 挂起协程,释放线程
    delay(100)
    mutex.unlock()
}

核心区别:synchronized/ReentrantLock 阻塞线程 ,Mutex 挂起协程,线程可以转去执行其他协程。

并发在协程中的选择
kotlin 复制代码
// 方案1:Atomic(适用于简单计数)
val count = AtomicInteger(0)
count.incrementAndGet() // 线程安全,但无法与挂起函数组合

// 方案2:Mutex(适用于需要保护挂起操作的场景)
val mutex = Mutex()
mutex.withLock {
    delay(100) // 锁内可以调用挂起函数
    count++
}

// 方案3:单线程调度器(适用于需要保证顺序执行的复杂逻辑)
val dispatcher = Dispatchers.Default.limitedParallelism(1)

3.3 StateFlow / SharedFlow ------ 状态流的并发安全

在 Android 开发中,StateFlow 是共享可变状态的最佳实践:

kotlin 复制代码
class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow() // 对外只读

    fun updateName(name: String) {
        // update 是原子的 CAS 操作,多线程并发安全
        _uiState.update { current ->
            current.copy(name = name)
        }
    }
}

关键设计:

  • 对外暴露只读 StateFlow,防止外部篡改
  • 使用 update {} 而不是直接 .value =,后者是「读-改-写」复合操作,并发下会丢更新
  • 配合不可变的 data class,天然线程安全

3.4 不可变性 ------ Kotlin 的默认姿势

不可变对象创建后状态不再改变,可以随意共享,Kotlin 在语法层面让这件事变得极其自然:

kotlin 复制代码
// Kotlin 默认鼓励不可变
val list = listOf(1, 2, 3)        // 不可变 List
data class User(val name: String) // val + data class,深拷贝用 copy()

// 共享不可变对象,无需任何同步
val user = User("Tom")
scope.launch { println(user.name) } // 随便用,不会出问题

实践优先级:不可变对象 > 线程隔离(ThreadLocal/单线程调度器) > 同步(锁/Atomic)


四、常见误区自查清单

  • ❌ 认为 volatile 能保证原子性 → 它只管可见性和有序性
  • ❌ 在协程里用 synchronized/ReentrantLock 长时间持锁 → 会阻塞底层线程,拖垮整个协程调度器
  • ❌ 认为 ConcurrentHashMap 的复合操作是原子的 → get + put 不是原子的,要用 computeIfAbsent
  • ❌ 在 ThreadLocal 用完不 remove() → 线程池场景内存泄漏
  • ❌ 直接暴露 MutableStateFlow → 外部可以随便改,破坏封装
  • ❌ 用 list.add() 更新 StateFlow 的值 → 应该用 update { it.copy(...) }

五、总结

  1. Java 的核心思路:优先不可变对象(final/record)从源头避免共享可变状态,必要时用锁(synchronized/Lock)、原子类(Atomic)、并发容器(ConcurrentXxx)来保护
  2. Kotlin 的核心思路 :优先用协程的结构化并发 + 不可变性从源头避免共享,必要时用 Mutex、StateFlow 等协程友好原语
  3. 通用最佳实践:不可变优先 → 线程隔离其次 → 加锁 / 原子类兜底
相关推荐
她说..2 小时前
IntelliJ IDEA 快捷键速查表(Mac 版)
java·macos·intellij-idea
EatFan3 小时前
JunoYi 框架实践:Spring Boot 项目为什么拆成 framework、module、server 三层?
java·spring boot·后端·framework·module·模块化·junoyi
Wang's Blog3 小时前
Java 项目实战: 外卖平台优化-从库Slave配置与主从复制验证
java·开发语言
hai_android3 小时前
子 View 被压小时,如何向父容器"告状"?——MEASURED_STATE_TOO_SMALL 向上传递机制
android·kotlin·android jetpack
子一!!3 小时前
集成Spring家族的Spring论坛实战==一阶段
java·后端·spring
何中应3 小时前
Maven 执行控制台中文乱码问题
java·maven·intellij-idea
Zhou1411363 小时前
SpringSecurity_01_入门与认证
java
java_nnnn3 小时前
JavaEE进阶-CSS初识
java·前端·css·java-ee·html
Wang's Blog3 小时前
Java 项目实战: 外卖平台优化-使用Redis作为缓存产品与TTL配置
java·redis·缓存