并发测试 (Lincheck)
在编写并发数据结构(如无锁队列、计数器)时,传统的单元测试很难覆盖所有可能的线程交替执行路径(Interleaving)。Lincheck 是 JetBrains 推出的并发测试框架,它通过 模型检查 (Model Checking) 技术,自动生成并发场景并验证操作的线性一致性 (Linearizability)。
核心价值
Lincheck 能帮你回答一个灵魂问题:“我写的这个并发类,在多线程环境下真的线程安全吗?”
它通过暴力枚举或随机探索线程的执行顺序,试图找到一个让你的代码崩溃或产生错误结果的执行路径。
依赖配置
kotlin
dependencies {
testImplementation("org.jetbrains.kotlinx:lincheck:2.30")
}基础用法:测试计数器
假设我们有一个简单的计数器,想验证它是否线程安全。
kotlin
class Counter {
private var value = 0 // ❌ 故意使用非线程安全的变量
fun inc(): Int = ++value
fun get(): Int = value
}编写 Lincheck 测试
- 定义操作:使用
@Operation标注要测试的方法。 - 配置策略:使用
ModelCheckingOptions。 - 运行测试:
LinChecker.check()。
kotlin
import org.jetbrains.kotlinx.lincheck.annotations.*
import org.jetbrains.kotlinx.lincheck.check
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelChecking.ModelCheckingOptions
import org.junit.Test
class CounterTest {
private val counter = Counter()
@Operation
fun inc() = counter.inc()
@Operation
fun get() = counter.get()
@Test
fun test() {
// 使用模型检查策略,它会分析所有可能的交替执行
ModelCheckingOptions()
.iterations(100) // 迭代次数
.invocationsPerIteration(1000) // 每次迭代的操作数
.check(this::class)
}
}测试结果:Lincheck 会迅速失败,并打印出导致错误的执行轨迹(Trace),告诉你:“看,如果线程 A 在读,线程 B 同时也写,结果就错了。”
测试策略:Stress vs Model Checking
Lincheck 提供两种核心策略:
1. 压力测试 (Stress Testing)
在真实的 JVM 线程上并发运行操作。这类似于传统的压力测试,但 Lincheck 会自动生成复杂的场景。
- 优点:速度快,能发现低级错误。
- 缺点:不能保证发现所有竞态条件(受 OS 调度影响)。
2. 模型检查 (Model Checking)
在受控环境中模拟并发。Lincheck 接管了所有同步原语(如 synchronized, Atomic, ReentrantLock),并穷举所有可能的线程调度点。
- 优点:确定性高,能发现极隐蔽的 Bug。
- 缺点:运行较慢,受限于内存模型。
进阶:验证一致性与状态
Lincheck 并不只是检查是否抛出异常,它最重要的功能是验证线性一致性 (Linearizability)。这意味着并发执行的结果必须等同于某种合法的单线程执行顺序。
指定基准实现
如果你的并发类比较复杂,你可以指定一个简单的、非线程安全的类作为基准(Sequential Specification)。
kotlin
class ConcurrentQueue<T> { ... }
// 基准实现:使用简单的 ArrayList 模拟队列行为
class SequentialQueue<T> {
private val list = mutableListOf<T>()
fun add(v: T) = list.add(v)
fun poll(): T? = if (list.isEmpty()) null else list.removeAt(0)
}
// 在测试配置中指定
ModelCheckingOptions()
.sequentialSpecification(SequentialQueue::class.java)
.check(ConcurrentQueueTest::class)状态验证 @Validate
有时你需要在每个操作序列结束后检查内部的不变量(Invariants)。
kotlin
@Validate
fun validateState() {
// 检查内部状态是否自相矛盾,例如:
// check(size >= 0)
}实战:验证 AtomicFU 算法
如果你使用 AtomicFU 实现了一个无锁栈(Stack),Lincheck 是验证其正确性的最佳工具。
kotlin
class ConcurrentStack<T> {
private val top = atomic<Node<T>?>(null)
@Operation
fun push(value: T) {
top.loop { cur ->
val newNode = Node(value, cur)
if (top.compareAndSet(cur, newNode)) return
}
}
@Operation
fun pop(): T? {
top.loop { cur ->
if (cur == null) return null
if (top.compareAndSet(cur, cur.next)) return cur.value
}
}
}核心工程准则
- 库开发者必修:如果你在开发通用的并发工具类(Utils),Lincheck 是必须集成的 CI 环节。
- 不仅仅是 Crash:Lincheck 不仅检查崩溃,还检查结果的正确性(它会与一个单线程的基准实现进行比对)。
- 配合 AtomicFU:Lincheck 对 Kotlin 的 AtomicFU 有着原生支持,能够完美模拟底层的 CAS 行为。