使用 `AtomicBoolean` 和 `AtomicReference` 的正确姿势:避免并发陷阱

642 阅读4分钟

在多线程编程中,确保线程安全是一个关键问题。AtomicBooleanAtomicReference 是 Java 提供的原子变量类,用于在多线程环境中安全地操作布尔值和引用类型。然而,错误地使用这些原子类可能会导致并发问题。本文将通过一个常见的错误用法示例,探讨如何正确使用 AtomicBooleanAtomicReference,并深入分析其工作原理和应用场景。

错误示例

以下是一个错误使用 AtomicBoolean 的示例:

private val isRegistered = AtomicBoolean(false)

override fun registerBroadcast() {
    if (isRegistered.get().not()) {
        registerBroadcast()
    }
}
    fun registerBroadcast(){
       isRegistered.set(true)
       *****
     }

在这个示例中,registerBroadcast 方法试图通过检查 isRegistered 的值来决定是否调用 registerBroadcast 方法。然而,这种用法存在潜在的并发问题。

问题分析

上述代码的问题在于 isRegistered.get().not()registerBroadcast() 之间没有任何同步机制。假设有两个线程同时调用 registerBroadcast 方法:

  1. 线程 A 调用 isRegistered.get().not(),返回 true
  2. 线程 B 也调用 isRegistered.get().not(),也返回 true
  3. 线程 A 调用 registerBroadcast()
  4. 线程 B 也调用 registerBroadcast()

结果是 registerBroadcast 方法被调用了两次,这显然不是预期的行为。

正确用法

为了确保 registerBroadcast 方法只被调用一次,我们需要使用 AtomicBoolean 的原子操作。compareAndSet 方法可以确保在检查和设置值时的原子性:

private val isRegistered = AtomicBoolean(false)

override fun registerBroadcast() {
    if (isRegistered.compareAndSet(false, true)) {
        registerBroadcast()
    }
}

在这个改进的版本中,compareAndSet(false, true) 方法会在 isRegistered 的值为 false 时将其设置为 true,并返回 true。如果 isRegistered 的值已经是 true,则返回 false。这样可以确保 registerBroadcast 方法只会被调用一次。

深入理解 AtomicBooleanAtomicReference

AtomicBooleanAtomicReference 是 Java 中 java.util.concurrent.atomic 包的一部分,它们提供了一种在多线程环境中安全操作布尔值和引用类型的方式。AtomicBooleanAtomicReference 使用了底层的硬件支持(如 CAS 操作)来确保操作的原子性。

应用场景

AtomicBooleanAtomicReference 适用于以下场景:

  1. 单次初始化:确保某个操作(如资源初始化、注册广播等)只执行一次。
  2. 状态标记:在多线程环境中标记某个状态(如任务是否完成、资源是否释放等)。
  3. 轻量级锁:在某些情况下,可以使用 AtomicBoolean 实现轻量级的锁机制。

实际案例:Android 系统源码中的 Lifecycle.coroutineScope

在 Android 系统源码中,有一个使用 AtomicReference 的示例,它确保 LifecycleCoroutineScope 只被初始化一次:

public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (internalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

public var internalScopeRef: AtomicReference<Any> = AtomicReference<Any>()

详细解释

  • internalScopeRef:这是一个 AtomicReference,用于存储 LifecycleCoroutineScopeImpl 的实例。
  • get 方法:在 get 方法中,首先尝试获取 internalScopeRef 的当前值。如果已经存在一个 LifecycleCoroutineScopeImpl 实例,则直接返回。
  • 创建新实例:如果 internalScopeRef 为空,则创建一个新的 LifecycleCoroutineScopeImpl 实例。
  • compareAndSet 方法:使用 compareAndSet(null, newScope) 方法确保只有一个线程能够成功将 internalScopeRefnull 设置为 newScope。如果成功,则注册并返回新的 LifecycleCoroutineScopeImpl 实例。

其他注意事项

  • 初始化:确保 AtomicBooleanAtomicReference 在使用前已经正确初始化。
  • 其他原子类:Java 提供了其他原子类,如 AtomicIntegerAtomicLongAtomicReference,它们也可以用于类似的场景。
  • 性能:虽然原子操作比锁更高效,但在高并发场景下,仍需注意性能问题。

实际案例

假设我们有一个需要在多线程环境中初始化的资源,我们可以使用 AtomicBoolean 来确保初始化操作只执行一次:

class ResourceInitializer {
    private val isInitialized = AtomicBoolean(false)

    fun initialize() {
        if (isInitialized.compareAndSet(false, true)) {
            // 执行初始化操作
            println("Resource initialized")
        } else {
            println("Resource already initialized")
        }
    }
}

fun main() {
    val initializer = ResourceInitializer()
    val threads = List(10) {
        Thread {
            initializer.initialize()
        }
    }
    threads.forEach { it.start() }
    threads.forEach { it.join() }
}

在这个示例中,无论有多少线程同时调用 initialize 方法,初始化操作只会执行一次。

总结

在多线程编程中,正确使用 AtomicBooleanAtomicReference 等原子类可以帮助我们避免并发问题。通过使用 compareAndSet 方法,我们可以确保操作的原子性,从而避免多个线程同时执行不安全的操作。希望本文能帮助你更好地理解和使用 AtomicBooleanAtomicReference,编写出更加健壮的多线程代码。

参考资料