Atomic原子操作详解
Atomic原子操作详解
原子操作是在并发编程中指不能被中断的、不可分割的操作。它要么完全执行,要么完全不执行,不会出现部分执行的情况。
并发原子操作理解
在并发编程中,原子操作是指不可被中断的、不可分割的最小操作单位。原子操作要么完全执行,要么完全不执行,不存在中间状态。
在多线程环境下,多个线程可以同时执行,因此可能出现竞争条件(Race Condition),即多个线程同时访问和修改共享的数据,导致数据不一致或不确定的结果。
原子操作可以帮助我们解决这个问题,通过确保某个操作以原子方式执行,从而避免了竞争条件。
在Java中,你可以通过以下几种方式来实现并发原子操作:
- 使用原子类:Java提供了java.util.concurrent.atomic包,其中包含了一系列的原子类,如AtomicInteger、AtomicLong等。这些原子类提供了一些常见的原子操作,如原子的读取、写入、比较和交换等。通过使用这些原子类,你可以在多线程环境中实现对共享数据的安全访问和修改。
- 使用锁:通过使用锁机制,比如synchronized关键字或ReentrantLock类,你可以确保同一时间只有一个线程可以访问被锁定的代码块或资源。通过在关键的代码段上加锁,可以保证原子性操作的执行。只有持有锁的线程才能执行被锁定的代码块,其他线程必须等待锁释放。
- 使用volatile关键字:将共享变量声明为volatile,可以确保变量的读取和写入操作具有可见性和有序性。volatile关键字保证了变量在多个线程之间的一致性,每次对volatile变量的读取都是从主内存中获取最新值,每次对volatile变量的写入都会立即刷新到主内存。
- 使用原子操作工具类:Java还提供了一些原子操作的工具类,如java.util.concurrent.atomic.AtomicIntegerArray和java.util.concurrent.atomic.AtomicReference等。这些工具类提供了对数组和引用类型的原子操作支持,可以在并发环境中实现对数组元素或引用对象的原子操作。
synchronized VS atomic
synchronized
synchronized 是 Java 中的关键字,用于实现线程同步和互斥访问共享资源。
通过使用synchronized关键字修饰代码块或方法,可以确保同一时间只有一个线程可以执行被锁定的代码块或方法。synchronized关键字提供了内置的互斥机制,它会自动获取锁和释放锁,保证了代码块或方法的原子性操作。在使用synchronized时,需要注意锁的粒度和范围,以避免死锁和性能问题。
atomic
atomic 是 Java 并发包 (java.util.concurrent.atomic)中提供的一组原子类。这些原子类提供了一些常见的原子操作,如原子的读取、写入、比较和交换等。它们通过使用底层的CAS(Compare-and-Swap)操作来实现线程安全的原子操作。CAS
是一种乐观锁机制,它利用硬件的原子性操作来实现对共享变量的并发修改。原子类中的方法都是原子的,不需要显式加锁,因此可以在高并发环境中获得较好的性能。使用原子类可以避免使用锁带来的开销和潜在的死锁问题。
选择使用 synchronized 还是 atomic 取决于具体的需求和场景。一般来说:
- 当只需要在特定的代码块或方法上实现原子操作时,可以选择使用synchronized。它更适合于复杂的同步逻辑和资源访问控制。
- 当只需要对单个变量进行原子操作时,可以选择使用atomic。它更适合于简单的操作,并且在高并发环境中性能更好。
atomic实现原子操作
java.util.concurrent.atomic 包中的原子类通过底层的CAS(Compare-and-Swap)操作来实现原子操作。CAS 是一种乐观锁机制,它利用硬件的原子性操作来实现对共享变量的并发修改。
CAS实现原理
CAS 操作包含三个参数:内存位置(变量)、期望值和新值。CAS 操作的执行过程如下:
- 通过读取内存位置的值,获取当前的变量值。
- 比较当前的变量值与期望值是否相等。如果相等,则说明变量值没有被其他线程修改,可以执行更新操作。 将新值写入内存位置,更新变量的值。
- 如果不相等,说明其他线程已经修改了变量的值,当前线程的更新操作失败。此时可以选择重试或执行其他逻辑。
CAS 操作是原子的,因为在执行比较和写入操作期间,不会被其他线程中断或干扰。它依赖于底层硬件的原子性操作,例如处理器提供的原子指令(例如 Compare-and-Swap、Load-Link/Store-Conditional)。
CAS存在的问题
自旋重试
CAS 操作失败时,线程需要不断地自旋重试,直到成功为止。这会消耗一定的 CPU 资源。在高并发情况下,多个线程同时进行 CAS 操作可能会导致大量的自旋重试,从而增加系统负载。
ABA 问题
CAS 操作只关注变量的值是否与期望值相等,而不考虑变量值在操作期间是否发生了变化。这可能引发 ABA 问题。例如,线程 A 将变量从初始值 A 修改为 B,然后再修改回 A,而线程 B 在此期间执行了一个操作,期望变量的值为 A,CAS操作会认为符合条件,导致操作成功。然而,线程 A 并不知道变量的值已经被修改过。解决 ABA 问题的方法之一是使用带有版本号的原子类(如AtomicStampedReference),以便在比较时同时考虑变量值和版本号。
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,可以使用循环CAS的方式保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原则性。
从JDk5开始提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里进行CAS操作
竞争条件
在高并发环境下,多个线程同时进行 CAS 操作,竞争同一个变量,可能会导致竞争条件的发生。如果多个线程同时执行 CAS 操作,只有一个线程的操作会成功,其他线程的操作会失败并进行重试。这种竞争会降低整体的性能。
内存模型限制
CAS 操作依赖于底层硬件的原子性指令,因此对内存模型的限制较大。在一些特殊的内存模型或平台上,CAS 操作可能不具备原子性,或者在使用过程中需要特殊的配置或处理。
LongAdder
LongAdder是JDK8时引入的一个原子类。主要解决的是在高并发环境下热点数据读写,对原子属性进行写操作时,通过写热点分散,减少竞争,它可以提供更好的性能和吞吐量。
与AtomicLong相比,LongAdder在高并发环境下通常具有更好的性能其原理就是因为LongAdder内部维护了一组变量,将计数器的值分散到这些变量中,不同线程对不同变量进行累加操作,从而减少了竞争。当需要获取计数器的总和时,LongAdder会将所有变量的值累加起来,得到最终的结果。
需要注意的是,LongAdder在某些情况下可能会产生略微的误差,因为它的结果是根据当前的内部状态计算得出的,并且可能会受到并发更新的影响。如果需要精确的结果,可以使用LongAccumulator或AtomicLong等其他适用的原子类。
import java.util.concurrent.atomic.LongAdder;
public class Example {
private LongAdder counter = new LongAdder();
public void increment() {
counter.increment();
}
public void decrement() {
counter.decrement();
}
public void add(long value) {
counter.add(value);
}
public long getCount() {
return counter.sum();
}
}
以上示例,使用LongAdder时,可以通过调用increment()、decrement()和add()等方法来进行计数的增加、减少和加法操作。