多线程:线程池、ExecutorService、synchronized、volatile、原子类

多线程开发,说白了就是让程序同时干好几件事。但你要是真让几百个线程同时跑,系统分分钟就崩了。我刚开始做Kotlin多线程时,就吃过这个亏——线程开太多,结果CPU切换上下文比干活还累。

所以,咱们得学会用工具来管理线程。今天聊的这几个,都是实战中绕不开的核心知识点。

1. 线程池:别自己new Thread()了

你想想看,每次来一个任务就new一个线程,那系统资源很快就耗尽了。线程池的作用,就是复用已有的线程,避免频繁创建和销毁。

Kotlin里用Java的线程池API,完全兼容。我个人习惯用Executors工具类来创建:

val executor = Executors.newFixedThreadPool(4)
executor.execute {
    println("任务执行中,线程:${Thread.currentThread().name}")
}
executor.shutdown()

这里newFixedThreadPool(4)表示核心线程数是4。什么意思?就是最多同时跑4个任务,多余的排队等着。

我的经验:核心线程数一般设为CPU核心数+1。比如4核CPU,设5个线程。I/O密集型的任务可以设多一些,比如2倍CPU核心数。

2. ExecutorService:更灵活的任务管理

ExecutorService是线程池的父接口,功能比Executor更强。比如你可以提交任务并获取返回值:

val service: ExecutorService = Executors.newCachedThreadPool()
val future: Future<String> = service.submit(Callable {
    Thread.sleep(1000)
    "任务完成"
})
println(future.get()) // 阻塞等待结果

这里newCachedThreadPool()会动态调整线程数,适合短时间大量小任务。但要注意——如果任务提交速度远大于处理速度,线程数会暴涨。我曾经在生产环境遇到过,一个接口被刷,结果线程池膨胀到上千个,直接把内存撑爆了。

避坑指南:生产环境慎用newCachedThreadPool()。我建议用newFixedThreadPool()或自定义ThreadPoolExecutor,设置最大线程数和队列容量。

3. synchronized:最简单的锁

多线程访问共享变量时,会出现数据不一致。比如两个线程同时给一个变量加1,结果可能只加了1次。为什么?因为count++不是原子操作,它分三步:读取、加1、写入。

synchronized就是给代码块加锁,保证同一时刻只有一个线程能执行:

class Counter {
    private var count = 0

    @Synchronized
    fun increment() {
        count++
    }

    fun getCount() = count
}

Kotlin里用@Synchronized注解,或者直接用synchronized()块:

val lock = Any()
synchronized(lock) {
    // 临界区代码
}

嗯,这里要注意:synchronized是重量级锁,性能开销大。如果只是读操作,没必要加锁。

4. volatile:轻量级的可见性保证

有时候我们不需要锁,只需要保证一个线程修改了变量,其他线程能立刻看到。这就是volatile的作用。

举个例子:一个标志位控制线程是否继续运行。

@Volatile
private var running = true

fun stop() {
    running = false
}

fun run() {
    while (running) {
        // 执行任务
    }
}

如果没有@Volatile,线程可能永远看不到running被改成false,因为每个线程会把变量缓存到本地内存里。说白了,volatile就是告诉JVM:别缓存,每次都从主内存读。

核心区别:
  • synchronized:保证原子性和可见性,但性能差
  • volatile:只保证可见性,不保证原子性

5. 原子类:无锁的线程安全

如果只是简单的加减操作,用synchronized太笨重了。Java提供了原子类,比如AtomicInteger,底层用CAS(比较并交换)实现,性能比锁高得多。

import java.util.concurrent.atomic.AtomicInteger

class AtomicCounter {
    private val count = AtomicInteger(0)

    fun increment() {
        count.incrementAndGet()
    }

    fun getCount() = count.get()
}

Kotlin里用起来和Java一样。我项目中统计接口调用次数,就用AtomicLong,几百万并发下毫无压力。

常用的原子类有:

  • AtomicInteger / AtomicLong:整数原子操作
  • AtomicBoolean:布尔值原子操作
  • AtomicReference<T>:对象引用原子操作
  • AtomicIntegerArray / AtomicLongArray:数组原子操作

6. 知识体系总览

下面这张图,帮你理清这几个概念的关系:

多线程核心知识体系 线程池 复用线程,控制并发数 ExecutorService 任务提交、结果获取 synchronized 重量级锁,保证原子性 volatile 轻量级,保证可见性 原子类 CAS无锁,高性能 对比总结 特性 线程池 synchronized volatile 原子类 原子性 -

7. 实战建议

说了这么多,到底怎么选?我总结几条经验:

  1. 控制并发数:用线程池,别裸new Thread()。固定线程池最稳。
  2. 简单计数:用原子类,性能最好。比如统计请求量、错误数。
  3. 状态标志:用volatile。比如开关、停止信号。
  4. 复杂同步:用synchronized或ReentrantLock。比如多步操作需要整体原子性。
小技巧:Kotlin的协程比线程更轻量,但底层原理还是离不开这些基础。先把线程池和原子类搞明白,学协程会轻松很多。

好了,多线程这块内容不少,但核心就这几个工具。你想想看,是不是比直接new Thread()省心多了?

公众号:蓝海资料掘金营,微信deep3321