【问题标题】:Is this use of AtomicBoolean a valid replacement for synchronized blocks?这种使用 AtomicBoolean 是同步块的有效替代品吗?
【发布时间】:2026-01-17 12:00:01
【问题描述】:

考虑两个不能同时执行的方法 a() 和 b()。 可以使用 synchronized 关键字来实现这一点,如下所示。我可以按照下面的代码使用 AtomicBoolean 达到相同的效果吗?

final class SynchonizedAB {

synchronized void a(){
   // code to execute
}

synchronized void b(){
  // code to execute
}

}

尝试使用 AtomicBoolean 实现与上述相同的效果:

final class AtomicAB {

private AtomicBoolean atomicBoolean = new AtomicBoolean();

void a(){
   while(!atomicBoolean.compareAndSet(false,true){

  }
  // code to execute
  atomicBoolean.set(false);
}

void b(){
    while(!atomicBoolean.compareAndSet(false,true){

   }
     // code to execute
     atomicBoolean.set(false);
    }

 }

【问题讨论】:

    标签: java locking atomic synchronized atomicboolean


    【解决方案1】:

    不,因为synchronized 会阻塞,而使用AtomicBoolean 你会忙着等待。

    两者都将确保一次只有一个线程可以执行该块,但是您想让您的 CPU 在 while 块上旋转吗?

    【讨论】:

    • 是的,我宁愿旋转以减少操作系统调用停止/启动线程的成本,因为在我使用它的情况下,执行代码很快,因此它不会旋转长。
    • @aranhakki 线程没有停止/启动,线程将被阻塞。有很大的不同。
    • 好吧,这很有趣,你能解释一下区别吗?我猜你的调用并没有涉及到操作系统,所以成本是 JVM 调用而不是操作系统调用?
    • 停止和启动线程意味着销毁和创建一个新线程,而阻塞线程只会让现有线程等待,直到它被唤醒并计划再次运行。您应该小心假设拥有自旋锁会带来任何性能优势。一些 java.util.concurrent 类确实使用了这种机制,所以它是有时间和地点的,但它是否是最好的解决方案是不确定的。至少性能测试正确。
    • 您必须权衡方法 1(同步)中上下文切换的成本与方法 2(CAS)中不必要的旋转的成本。使用 CAS,线程失去了做一些有意义的事情的机会,而是在 while 循环中旋转。
    【解决方案2】:

    这取决于您计划使用原始同步版本的代码实现什么。如果在原始代码中添加同步只是为了确保在 a 或 b 方法中一次只存在一个线程,那么对我来说两个版本的代码看起来相似。

    但是,Kayaman 提到的差异很少。还要添加更多差异,使用同步块,您将获得内存屏障,您将在原子 CAS 循环中错过。但是如果方法的主体不需要这样的障碍,那么这种差异也会被消除。

    在单独的情况下,原子 cas 循环是否比同步块表现更好,只有性能测试可以判断,但这是在并发包中的多个地方遵循相同的技术,以避免块级别的同步。

    【讨论】:

      【解决方案3】:

      从行为的角度来看,这似乎是 Java 内置同步(监视器锁)的部分替代品。特别是,它似乎提供了正确的互斥,这是大多数人在使用锁时所追求的。

      它似乎也提供了正确的内存可见性语义。 Atomic* 系列类具有与volatile 相似的内存语义,因此释放其中一个“锁”将提供与另一个线程获取“锁”的发生前关系,这将提供您想要的可见性保证。

      这与 Java 的 synchronized 块的不同之处在于它不提供在异常情况下的自动解锁。要获得与这些锁相似的语义,您必须将锁定和用法包装在 try-finally 语句中:

      void a() {
          while (!atomicBoolean.compareAndSet(false, true) { }
          try {
              // code to execute
          } finally {
              atomicBoolean.set(false);
          }
      }
      

      b 类似)

      这种结构似乎确实提供了与 Java 的内置监视器锁类似的行为,但总的来说,我觉得这种努力是错误的。从another answer 上的 cmets 看来,您似乎有兴趣避免阻塞线程的操作系统开销。发生这种情况时肯定会有开销。但是,Java 的内置锁已经过大量优化,在短期竞争的情况下提供了非常便宜的非竞争锁、偏向锁和自适应自旋循环。在许多情况下,这些尝试中的最后一个是避免操作系统级别的阻塞。通过实现自己的锁,您就放弃了这些优化。

      当然,您应该进行基准测试。如果您的性能受到操作系统级阻塞开销的影响,则可能是您的锁太粗糙了。减少锁的数量或拆分锁可能比尝试实现自己的锁更有效地减少争用开销。

      【讨论】:

      • 几乎无限的while循环不会产生CPU或其他开销吗?
      • @Jus12 是的,这种“自旋等待”或“自旋锁定”技术可能会产生一些开销。但是阻塞一个线程,安排它被通知,然后再次唤醒它也会产生开销和延迟。这里的赌注是通过一个 CAS 循环的次数可能很少,因此与阻塞和唤醒相比,开销很小。