Java 中的 Atomic 与 Synchronized 对比

2025 年 1 月 6 日 | 阅读 5 分钟

并发编程涉及多个线程并行执行,这可以显著提高应用程序的性能。然而,管理并发执行可能会导致复杂的问题,例如竞态条件,即多个线程同时尝试修改同一变量,导致不可预测的行为。

Java 提供了多种机制来安全地处理并发。在这些机制中,`synchronized` 块和方法一直是确保互斥的传统方式,而 Java 5 中 `java.util.concurrent.atomic` 包的引入为特定用例提供了一种更有效的方法。在本节中,我们将讨论 **Atomic 类和 synchronized 方法之间的区别**,重点介绍它们的用例、性能影响和最佳实践。

Synchronized 关键字

`synchronized` 关键字在 Java 中用于控制多个线程对代码关键部分的访问。它确保一次只有一个线程可以执行 `synchronized` 块或方法,从而提供了一种互斥机制。

Synchronized 如何工作?

当一个线程进入 `synchronized` 块或方法时,它会获取对象或类的锁。尝试进入 `synchronized` 块或方法的其他线程将被阻塞,直到锁被释放。以下是一个 `synchronized` 方法的示例:

在上面的示例中,`increment` 和 `getCount()` 方法是 `synchronized` 的,确保一次只有一个线程可以修改或读取 `count` 变量。

性能考虑

虽然 `synchronized` 确保了线程安全,但它可能会引入显著的开销。当一个线程尝试获取锁时,如果另一个线程持有锁,它可能不得不等待。这可能导致争用,特别是在高度并发的应用程序中,其中多个线程频繁访问 `synchronized` 方法。

此外,获取和释放锁的成本可能很高。这种开销包括操作系统在管理线程调度和上下文切换上花费的时间,这可能会在争用较高的情况下降低性能。

java.util.concurrent.atomic 包

`java.util.concurrent.atomic` 包提供了支持对单个变量进行无锁、线程安全操作的类。这些类,如 `AtomicInteger`、`AtomicLong` 和 `AtomicReference`,使用现代 CPU 支持的底层原子操作来确保线程安全,而无需 `synchronized`。

Atomic 类如何工作?

Atomic 类利用比较并交换 (CAS) 操作来实现线程安全。CAS 是一种底层的原子指令,它仅在变量持有特定值时才更新它,从而确保原子地执行更新。以下是使用 `AtomicInteger` 的示例:

在此示例中,`increment` 方法使用 `AtomicInteger` 的 `incrementAndGet` 方法,该方法原子地递增计数,而无需显式的 `synchronized`。

性能考虑

Atomic 类可以显著提高高度并发应用程序的性能。由于它们避免了获取和释放锁的开销,因此可以减少争用并提高可伸缩性。但是,原子操作仅适用于单变量更新。对于涉及多个变量的更复杂操作,可能需要传统的 `synchronized` 或其他并发机制。

Atomic 与 Synchronized 对比

比较基础Atomic同步
目的线程安全的单变量更新。线程安全的块或方法。
实施无锁,使用 CAS (Compare-And-Swap)。基于锁,使用内在锁。
开销
性能低争用、高并发场景更佳。在高争用下可能降级。
用例对单个变量的简单操作。涉及多个变量的复杂操作。
重入性不适用。支持重入。
公平性不适用。不保证公平性。
死锁无死锁。可能发生,需要仔细设计。
争用争用减少。争用较高
细粒度锁不适用。可以使用更细粒度的锁
读写访问不直接支持。使用 ReadWriteLock 进行读写访问。
Volatile 关键字不需要。可用于减少可见性问题。
代码复杂度对单变量操作更简单。对多变量操作更复杂。
可扩展性高可伸缩性。可伸缩性有限。
示例类AtomicInteger, AtomicLong, AtomicReferenceSynchronized 方法或块。
初始化简单直接。可以使用双重检查锁定或持有者模式。
饥饿无饥饿。可能发生线程饥饿。
使用场景计数器、标志、序列。修改多个资源的临界区。
避免常见陷阱通常比较直接。需要小心以避免死锁并确保正确的锁定顺序。
API 可用性自 Java 5 (java.util.concurrent.atomic) 起。自 Java 1.0 起。

高级注意事项

重入性

`synchronized` 方法和块是可重入的,这意味着持有锁的线程可以再次获取它而不会被阻塞。这对于递归方法或方法调用同一对象上的另一个 `synchronized` 方法特别有用。

相反,Atomic 类本身不直接支持重入。每个原子操作都是独立的,不存在持有锁的概念。

公平性

`synchronized` 关键字不保证公平性。等待获取锁的线程不一定按照它们请求的顺序获得访问权。在某些情况下,这可能导致线程饥饿。相比之下,`java.util.concurrent` 包提供了显式锁(如 `ReentrantLock`),可以配置公平性策略。

死锁

如果在 `synchronized` 代码中,两个或多个线程尝试以不同的顺序获取锁,则可能发生死锁。避免死锁需要仔细的设计并遵守一致的锁定顺序。Atomic 类是无锁的,因此不会发生死锁。

在 Atomic 和 Synchronized 之间进行选择

单变量更新:优先选择 Atomic 类,因为它们简单且具有性能优势。它们对于计数器、标志和其他单变量状态管理特别有用。

复杂操作:对于涉及多个变量的复杂操作或 Atomic 操作不足时,请使用 `synchronized`。确保 `synchronized` 方法保持简短高效,以最大程度地减少争用。

读写锁:对于读写比例很高的场景,请考虑使用读写锁 (ReentrantReadWriteLock)。它允许多个读取者并发访问共享数据,同时仍为写入者提供独占访问。

最小化争用

细粒度锁:使用更细粒度的锁来减少争用。不要同步大段代码,只同步需要独占访问的关键部分。

锁分段:将负载分散到多个锁上。例如,哈希表可以为每个桶使用一个单独的锁,从而与单个全局锁相比减少了争用。

不可变对象:尽可能优先使用不可变对象。不可变对象本质上是线程安全的,无需 `synchronized` 或 Atomic 操作。

避免常见陷阱

双重检查锁定:在使用双重检查锁定 (Double-Checked Locking) 时要小心。虽然它可以提高性能,但它需要仔细实现以避免细微的错误。对于安全高效的延迟初始化,优先选择延迟初始化持有者模式 (initialization-on-demand holder idiom)。

Volatile 关键字:对于被多个线程访问但不需要完全 `synchronized` 的变量,请使用 `volatile` 关键字。Volatile 变量提供了一种轻量级的机制来确保可见性,而无需互斥。

Atomic 和 `synchronized` 机制都在 Java 的并发领域发挥着至关重要的作用。Atomic 类利用底层原子指令,为单变量操作提供了高性能且可伸缩的解决方案,从而在没有 `synchronized` 开销的情况下确保线程安全。

另一方面,`synchronized` 方法和块为涉及多个变量的复杂操作提供了通用且强大的解决方案,尽管它们的开销较高。

选择适合您需求的工具需要了解您应用程序的具体要求,并仔细权衡性能和复杂性。通过利用 Atomic 类和 `synchronized` 的优势,您可以构建高效且健壮的 Java 并发应用程序。