1. 概述
在这个教程中,我们将探讨Java原子类(如AtomicInteger
和AtomicReference
)中的set()
和lazySet()
方法之间的差异。
2. 原子变量 - 快速回顾
Java中的原子变量使我们能够在不添加诸如监视器或互斥锁之类的并发原语的情况下,轻松地在类引用或字段上执行线程安全操作。
它们定义在java.util.concurrent.atomic
包下,尽管不同类型的原子类型具有不同的API,但大多数都支持set()
和lazySet()
方法。
为了简化,本文将使用AtomicReference
和AtomicInteger
,但同样的原则也适用于其他原子类型。
3. set()
方法
set()
方法等同于写入一个 volatile字段。
调用set()
后,如果从另一个线程使用get()
方法访问该字段,更改会立即可见。这意味着值已从CPU缓存刷新到所有CPU核心共有的内存层。
让我们通过创建一个简单的生产者-消费者控制台应用来展示上述功能:
public class Application {
AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) {
Application app = new Application();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
app.atomic.set(i);
System.out.println("Set: " + i);
Thread.sleep(100);
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
synchronized (app.atomic) {
int counter = app.atomic.get();
System.out.println("Get: " + counter);
}
Thread.sleep(100);
}
}).start();
}
}
在控制台上,我们应该看到一系列的"Set"和"Get"消息:
Set: 3
Set: 4
Get: 4
Get: 5
表明缓存一致性的是,"Get"语句中的值总是等于或大于其上方的"Set"语句中的值。
这种行为虽然非常有用,但也伴随着性能损失。在不需要缓存一致性的情况下,我们希望能够避免这种情况。
4. lazySet()
方法
lazySet()
方法与set()
方法相同,但不进行缓存刷新。
换句话说,我们的更改最终对其他线程可见。这意味着从更新后的AtomicReference
在不同线程上调用get()
可能会得到旧值。
要在实际操作中看到这一点,让我们改变之前控制台应用中第一个线程的Runnable
:
for (int i = 0; i < 10; i++) {
app.atomic.lazySet(i);
System.out.println("Set: " + i);
Thread.sleep(100);
}
新的"Set"和"Get"消息可能不会连续递增:
Set: 4
Set: 5
Get: 4
Get: 5
由于线程的特性,可能需要多次运行应用才能触发这种行为。消费者线程首先获取值4,即使生产者线程已经将AtomicInteger
设置为5,这意味着在使用lazySet()
时,系统是最终一致的。
用更技术的语言来说,我们可以说lazySet()
方法不会在代码中形成发生前边(happens-before)关系,与set()
方法不同。
5. 何时使用lazySet()
方法
由于lazySet()
与set()
方法的区别微妙,我们不一定能立即确定何时使用它。我们需要仔细分析问题,不仅要确保性能提升,还要确保多线程环境下的正确性。
我们可以使用它的一个场景是,在不再需要对象引用时将其替换为null
。 这样,我们表示对象可以被垃圾回收,而不会产生任何性能影响。我们假设其他线程可以在看到AtomicReference
为null
之前继续使用过时的值。
通常情况下,当我们要对原子变量进行更改,并且知道这个更改不需要立即对其他线程可见时,我们应该使用lazySet()
方法。
6. 总结
在这篇文章中,我们研究了原子类中set()
和lazySet()
方法之间的差异,以及何时使用它们。如往常一样,示例代码可在GitHub上找到:GitHub链接。