多线程:线程池、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个任务,多余的排队等着。
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. 知识体系总览
下面这张图,帮你理清这几个概念的关系:
7. 实战建议
说了这么多,到底怎么选?我总结几条经验:
- 控制并发数:用线程池,别裸new Thread()。固定线程池最稳。
- 简单计数:用原子类,性能最好。比如统计请求量、错误数。
- 状态标志:用volatile。比如开关、停止信号。
- 复杂同步:用synchronized或ReentrantLock。比如多步操作需要整体原子性。
好了,多线程这块内容不少,但核心就这几个工具。你想想看,是不是比直接new Thread()省心多了?