Java原子变量中的set和lazyset之间的差异
1. 概述
在本教程中,我们将研究AtomicInteger和AtomicReference等Java原子 类的*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
由于线程的性质,我们可能需要重新运行应用程序才能触发此行为。即使生产者线程已将AtomicInteger设置为 5,消费者线程首先检索值 4 的事实意味着在使用*lazySet()*时系统最终是一致 的。
用更专业的术语来说,我们说*lazySet()*方法不充当代码中的happens-before 边缘,与其对应的*set()*方法相反。
5. 何时使用lazySet()
我们何时应该使用*lazySet()尚不清楚,因为它与set()*的区别很微妙。我们需要仔细分析问题,不仅要确保我们将获得性能提升,还要确保在多线程环境中的正确性。
**我们可以使用它的一种方法是在不再需要对象引用时将其替换为null。**这样,我们就表明该对象有资格进行垃圾回收,而不会产生任何性能损失。我们假设其他线程可以使用已弃用的值,直到他们看到AtomicReference为null。
但是,一般来说,当我们想要对原子变量进行更改时,我们应该使用lazySet(),并且我们知道更改不需要立即对其他线程可见。