Java 中的 volatile 与原子变量
Java 中的 volatile 与原子变量
1. 概述
在本教程中,我们将学习 volatile 关键字和原子类之间的区别以及它们解决的问题。 首先,需要了解 Java 如何处理线程之间的通信以及可能出现的意外问题。
线程安全是一个关键主题,它提供了对多线程应用程序内部工作的洞察。我们还将讨论竞态条件,但不会深入这个话题。
2. 并发问题
让我们通过一个简单的例子来了解原子类和 volatile 关键字间的区别。假设我们正在尝试创建一个在多线程环境中工作的计数器。
理论上,任何应用程序线程都可以增加这个计数器的值。让我们以一种天真的方法开始实现它,并检查会出现什么问题:
public class UnsafeCounter {
private int counter;
int getValue() {
return counter;
}
void increment() {
counter++;
}
}
这是一个完全工作的计数器,但不幸的是,它只适用于单线程应用程序。这种方法在多线程环境中会遭受可见性和同步问题。 在大型应用程序中,这可能会增加跟踪错误和甚至破坏用户数据的难度。
3. 可见性问题
在多线程应用程序中工作时,可见性问题是一个问题之一。可见性问题与 Java 内存模型密切相关。
在多线程应用程序中,每个线程都有自己的共享资源的缓存版本,并根据事件或计划从主内存中更新值。
线程缓存和主内存的值可能不同。 因此,即使一个线程在主内存中更新了值,这些更改也不会立即对其他线程可见。这就是所谓的可见性问题。
volatile 关键字通过绕过本地线程的缓存来帮助我们解决这个问题。 因此,volatile 变量对所有线程都是可见的,所有这些线程都会看到相同的值。因此,当一个线程更新值时,所有线程都会看到新值。我们可以将其视为低级别的观察者模式,并可以重写之前的实现:
public class UnsafeVolatileCounter {
private volatile int counter;
public int getValue() {
return counter;
}
public void increment() {
counter++;
}
}
上述示例改进了计数器并解决了可见性问题。然而,我们仍然有一个同步问题,我们的计数器在多线程环境中不会正确工作。
4. 同步问题
尽管 volatile 关键字帮助我们解决了可见性问题,但我们仍然面临另一个问题。 在我们的增加示例中,我们对变量 count 执行了两个操作。首先,我们读取这个变量,然后为其分配一个新值。** 这意味着增加操作不是原子的。**
我们在这里面临的是一个竞态条件。 每个线程应该首先读取值,增加它,然后将其写回。当多个线程开始处理值,并在另一个线程写入之前读取它时,问题就会发生。
这样,一个线程可能会覆盖另一个线程写入的结果。synchronized 关键字可以解决这个问题。然而,这种方法可能会创建瓶颈,并且不是解决这个问题的最优雅的方法。
5. 原子值
原子值提供了一种更好、更直观的方式来处理这个问题。它们的接口允许我们在没有同步问题的情况下与值进行交互和更新。
在内部,原子类确保在这种情况下,增加将是一个原子操作。 因此,我们可以使用它来创建一个线程安全的实现:
public class SafeAtomicCounter {
private final AtomicInteger counter = new AtomicInteger(0);
public int getValue() {
return counter.get();
}
public void increment() {
counter.incrementAndGet();
}
}
我们最终的实现是线程安全的,并且可以在多线程应用程序中使用。 它与我们的第一个示例没有显著不同,只有通过使用原子类,我们才能解决多线程代码中的可见性和同步问题。
6. 结论
在本文中,我们了解到在多线程环境中工作时,我们必须非常小心。 错误和问题可能很难追踪,可能在调试时不会出现。这就是为什么了解 Java 如何处理这些情况至关重要。
volatile 关键字可以帮助解决可见性问题,并解决本质上是原子操作的问题。 设置一个标志是 volatile 关键字可能有帮助的一个例子。
原子变量有助于处理像增加-减少这样的非原子操作,或者任何需要在分配新值之前读取值的操作。原子值是一种简单方便的方法,可以解决我们代码中的同步问题。 如往常一样,示例的源代码可以在 GitHub 上找到。