Skip to content

并发测试 (Lincheck)

源:kotlinx.lincheck

在编写并发数据结构(如无锁队列、计数器)时,传统的单元测试很难覆盖所有可能的线程交替执行路径(Interleaving)。Lincheck 是 JetBrains 推出的并发测试框架,它通过 模型检查 (Model Checking) 技术,自动生成并发场景并验证操作的线性一致性 (Linearizability)

核心价值

Lincheck 能帮你回答一个灵魂问题:“我写的这个并发类,在多线程环境下真的线程安全吗?”

它通过暴力枚举或随机探索线程的执行顺序,试图找到一个让你的代码崩溃或产生错误结果的执行路径。

依赖配置

查看 Maven Central 最新版本

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 测试

  1. 定义操作:使用 @Operation 标注要测试的方法。
  2. 配置策略:使用 ModelCheckingOptions
  3. 运行测试: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
        }
    }
}

核心工程准则

  1. 库开发者必修:如果你在开发通用的并发工具类(Utils),Lincheck 是必须集成的 CI 环节。
  2. 不仅仅是 Crash:Lincheck 不仅检查崩溃,还检查结果的正确性(它会与一个单线程的基准实现进行比对)。
  3. 配合 AtomicFU:Lincheck 对 Kotlin 的 AtomicFU 有着原生支持,能够完美模拟底层的 CAS 行为。