Kotlin协程之Mutex详解

432 阅读3分钟

一、Mutex简介

Mutex(互斥锁)  是Kotlin协程中用于实现互斥访问共享资源的核心工具。它确保同一时间只有一个协程可以进入临界区(Critical Section),从而避免数据竞争和不一致问题。与线程锁不同,协程的Mutex基于挂起机制而非阻塞,更轻量且适合协程的协作式多任务模型。

二、核心特性

  1. 互斥访问:同一时刻仅允许一个协程持有锁。
  2. 挂起而非阻塞:未获取锁的协程会被挂起,不占用线程资源。
  3. 可取消性:协程被取消时自动释放锁,避免死锁。
  4. 不可重入:同一协程重复获取锁会导致挂起(需手动设计避免)。

三、使用场景

  1. 共享变量修改:如多个协程并发修改计数器。
  2. 复杂资源操作:操作非线程安全的集合(如ArrayList)。
  3. 跨协程状态同步:确保多个协程按特定顺序访问资源。
  4. 替代synchronized:在协程环境中更高效地实现互斥。

四、示例代码

1. 基本用法(保护计数器)

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val mutex = Mutex()
var counter = 0

fun main() = runBlocking {
    val jobs = List(10) { // 启动10个协程
        launch(Dispatchers.Default) {
            repeat(1000) { // 每个协程递增计数器1000次
                mutex.withLock { // 自动加锁/解锁
                    counter++
                }
            }
        }
    }
    jobs.forEach { it.join() }
    println("Final counter: $counter") // 正确输出10000
}

2. 保护共享集合

val mutex = Mutex()
val sharedList = mutableListOf<Int>()

fun updateList(value: Int) = runBlocking {
    mutex.withLock {
        sharedList.add(value)
        println("Added $value, list size: ${sharedList.size}")
    }
}

// 多个协程并发调用 updateList()

3. 结合超时控制

suspend fun tryLockWithTimeout() {
    try {
        // 尝试在500ms内获取锁
        if (mutex.tryLock(500)) {
            try {
                // 临界区操作
                delay(300) // 模拟耗时操作
            } finally {
                mutex.unlock()
            }
        } else {
            println("Failed to acquire lock within timeout")
        }
    } catch (e: CancellationException) {
        println("Lock acquisition cancelled")
    }
}

五、API详解

方法描述
mutex.lock()挂起协程直至获取锁,需手动调用unlock()
mutex.unlock()释放锁,必须在持有锁的协程中调用。
mutex.withLock { ... }自动管理锁的获取和释放(推荐方式)。
mutex.tryLock()非挂起方式尝试立即获取锁,返回true/false
mutex.holdsLock()检查当前协程是否持有锁。

六、注意事项

1. 避免死锁:

  • 不可重入:同一协程重复获取锁会导致永久挂起。

    // 错误示例:导致死锁
    mutex.withLock {
        mutex.withLock { // 第二次尝试获取同一锁
            // 永远不会执行
        }
    }
    

2. 锁粒度控制:

  • 尽量缩小临界区范围,避免长时间持有锁。

    // 不推荐:锁内执行耗时操作
    mutex.withLock {
        delay(1000) // 长时间挂起阻塞其他协程
        updateResource()
    }
    

3. 协程取消处理:

  • 协程取消时自动释放锁,但需确保资源状态一致性。

    mutex.withLock {
        try {
            criticalOperation()
        } finally {
            // 清理资源(即使协程被取消)
        }
    }
    

3. 性能考量:

  • 高频锁竞争场景中,优先考虑原子变量(如AtomicInteger)或无锁数据结构。

七、与其他并发工具对比

工具特点
Mutex互斥锁,适合保护共享资源的独占访问。
Semaphore信号量,允许指定数量的协程同时访问资源。
Atomic原子变量,轻量级解决简单变量的线程安全问题。
Channel通过通信共享内存,适合生产者-消费者模式。

八、总结

  • 何时使用Mutex:需要互斥访问复杂共享状态时。

  • 最佳实践

    • 优先使用withLock简化锁管理。
    • 避免在锁内执行耗时或挂起操作。
    • 结合协程取消机制确保资源释放。
  • 替代方案:简单场景下优先考虑原子变量或协程通道(Channel)。

通过合理使用Mutex,可以在协程并发编程中有效避免数据竞争,同时保持代码的高效与可维护性。

更多分享

  1. 一文吃透Kotlin中冷流(Clod Flow)和热流(Hot Flow)
  2. 一文带你吃透Kotlin协程的launch()和async()的区别
  3. Kotlin 作用域函数(let、run、with、apply、also)的使用指南
  4. Android 详解:高频使用的 8 种设计模式的核心思想和代码实现
  5. 一文带你吃透Kotlin中 lateinit 和 by lazy 的区别和用法