Java 中的原子变量简介-Java快速进阶教程
1. 简介
简而言之,当涉及并发时,共享可变状态很容易导致问题。如果未正确管理对共享可变对象的访问,应用程序很快就会变得容易出现一些难以检测的并发错误。
在本文中,我们将重新审视使用锁来处理并发访问,探讨与锁相关的一些缺点,最后引入原子变量作为替代方案。
2. 锁
让我们来看看:
public class Counter {
int counter;
public void increment() {
counter++;
}
}
在单线程环境中,这非常有效;但是,一旦我们允许多个线程写入,我们就会开始得到不一致的结果。
这是因为简单的增量操作 (counter++),它可能看起来像一个原子操作,但实际上是三个操作的组合:获取值、递增和写回更新的值。
如果两个线程尝试同时获取和更新值,则可能会导致更新丢失。
管理对象访问的方法之一是使用锁。这可以通过在增量方法签名中使用同步关键字来实现。sync关键字确保一次只有一个线程可以进入该方法(要了解有关锁定和同步的更多信息,请参阅 –Java 中的同步关键字指南):
public class SafeCounterWithLock {
private int counter;
public synchronized void increment() {
counter++;
}
}
使用锁可以解决问题。但是,性能受到打击。
当多个线程尝试获取锁时,其中一个线程获胜,而其余线程被阻止或挂起。
挂起然后恢复线程的过程非常昂贵,并且会影响系统的整体效率。
在小程序中,例如计数器,在上下文切换上花费的时间可能比实际代码执行要多得多,从而大大降低了整体效率。
3. 原子操作
有一个研究分支专注于为并发环境创建非阻塞算法。这些算法利用低级原子机指令(如比较和交换 (CAS))来确保数据完整性。
典型的 CAS 操作适用于三个操作数:
- 要操作的内存位置 (M)
- 变量的现有期望值 (A)
- 需要设置的新值 (B)
CAS 操作以原子方式将 M 中的值更新为 B,但前提是 M 中的现有值与 A 匹配,否则不执行任何操作。
在这两种情况下,都返回 M 中的现有值。这将三个步骤(获取值、比较值和更新值)组合到单个计算机级别的操作中。
当多个线程尝试通过 CAS 更新相同的值时,其中一个线程获胜并更新该值。但是,与锁的情况不同,没有其他线程被挂起;相反,他们只是被告知他们没有设法更新值。然后,线程可以继续执行进一步的工作,并完全避免上下文切换。
另一个后果是核心程序逻辑变得更加复杂。这是因为我们必须处理 CAS 操作不成功的场景。我们可以一次又一次地重试它,直到它成功,或者我们可以什么都不做,根据用例继续前进。
4. Java中的原子变量
Java 中最常用的原子变量类是 AtomicInteger、AtomicLong、AtomicBoolean和AtomicReference。这些类分别表示可以原子更新的整数、长整型、布尔值和对象引用。这些类公开的主要方法是:
-
Get()——从内存中获取值,这样其他线程所做的更改就可见了;相当于读取一个volatile变量
-
incrementAndGet() -在当前值上原子地加1
-
Set()——将值写入内存,这样更改对其他线程可见;相当于写一个易失性变量
-
lazySet()——最终将值写入内存,可能会用后续相关的内存操作重新排序。一个用例是为了垃圾收集而使引用无效,而这些引用永远不会再被访问。在这种情况下,通过延迟空易失性写入可以获得更好的性能
-
compareAndSet()——与第3节中描述的相同,当成功时返回true,否则返回false
-
weakCompareAndSet()——与第3节中描述的相同,但在某种意义上更弱,即它不创建happens-before顺序。这意味着它不一定会看到对其他变量的更新。从Java 9开始,这个方法在所有原子实现中都已弃用,取而代之的是weakCompareAndSetPlain()。weakCompareAndSet()的内存效果很简单,但是它的名字暗示了volatile内存效果。为了避免这种混淆,他们弃用了这个方法,并添加了四个具有不同内存效果的方法,如weakCompareAndSetPlain()或weakCompareAndSetVolatile()
使用AtomicInteger实现的线程安全计数器如下例所示:
public class SafeCounterWithoutLock {
private final AtomicInteger counter = new AtomicInteger(0);
int getValue() {
return counter.get();
}
void increment() {
counter.incrementAndGet();
}
}
如您所见,我们使用 incrementAndGet() 作为同步代码块,获取当前值,递增 1 并分配计数器变量的新值,最后将其存储在内存中。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构