友情提示:
本文原创&首发于公众号:程序员古德
原文标题:Java并发基础:原子类之AtomicInteger全面解析
本文概要
AtomicInteger
类提供了线程安全的整数操作,它通过利用底层硬件的原子性指令,能够在多线程环境中高效地实现整数的无锁更新,避免了传统同步机制带来的性能开销,在高并发场景下成为计数器可大幅提高程序的执行效率,同时又保证了数据一致性。
AtomicInteger核心概念
场景举例
模拟一个业务场景。
假设,有一个在线音乐媒体服务,在这个服务中,每首歌曲都有一个“喜欢”的计数器,每当有用户点击喜欢按钮时,这个计数器就会增加。由于这个系统的用户量很多,同一时间可能有成百上千的用户在点击喜欢按钮,因此这个“喜欢”计数器的增加操作必须在多线程环境下是安全的。
在这个模拟场景中,推荐使用AtomicInteger
解决这个问题。可以将每首歌曲的“喜欢”计数器实现为一个AtomicInteger
对象,每当有用户点击喜欢按钮时,就调用这个AtomicInteger
对象的incrementAndGet()
方法,这个方法会原子性地增加计数器的值,并返回增加后的结果,由于这个增加操作是原子的,因此不需要担心多个线程同时修改计数器时会导致数据不一致的问题。
技术思路
AtomicInteger
类是一个用于处理整数类型数据的原子类,它属于java.util.concurrent.atomic
包,主要用于解决多线程并发访问和修改共享整数变量时可能出现的数据不一致问题。
在多线程环境中,如果多个线程同时对同一个共享变量进行修改,就可能会引发数据不一致的情况,因为线程之间的操作是相互独立的,它们对共享变量的读取和修改操作可能会相互干扰。
为了避免这种情况,通常需要对共享变量的访问使用加锁进行同步处理,确保同一时间只有一个线程能够访问该变量。但是,使用加锁同步处理会带来一定的性能开销,因为它会阻塞线程的执行,但是,AtomicInteger
的内部使用了硬件级别的原子操作来保证多线程环境下对共享变量的安全访问和修改。
AtomicInteger
类提供了一系列原子操作的方法,如incrementAndGet()
、decrementAndGet()
、addAndGet()
等,这些方法可以确保在多线程环境下对整数变量的增加、减少和设置等操作是原子的,即不可被中断的,因此,在这些方法执行期间,其他线程无法访问或修改该变量,从而保证了数据的一致性。
AtomicInteger使用案例
下面是一个简单的Java代码示例,演示了如何使用AtomicInteger
类,代码中模拟多个线程同时对一个共享计数器进行增加操作,以展示AtomicInteger
如何保证操作的原子性和线程安全,如下代码:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDemo {
// 使用AtomicInteger作为共享计数器
private static AtomicInteger sharedCounter = new AtomicInteger(0);
public static void main(String[] args) {
// 启动5个线程,每个线程将对共享计数器增加100次
for (int i = 0; i < 5; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
sharedCounter.incrementAndGet(); // 原子性地增加计数器的值
}
}).start();
}
// 为了演示效果,主线程休眠一段时间,等待所有子线程执行完毕
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印最终计数器的值
System.out.println("Final Counter Value: " + sharedCounter.get());
}
}
在这段代码中:
- 创建了一个
AtomicInteger
实例sharedCounter
,并将其初始化为0,这个实例将作为多个线程共享的计数器。 - 在
main
方法中,启动了5个线程,每个线程内部执行一个循环,循环100次,每次循环都调用sharedCounter.incrementAndGet()
方法来原子性地增加计数器的值。 - 由于启动了5个线程,并且每个线程都会增加计数器100次,所以在没有线程安全问题的情况下,最终计数器的值应该是500。
AtomicInteger核心API
AtomicInteger
类提供了对整数进行原子操作的机制,这些原子操作在多线程环境中特别有用,因为它们可以保证对整数的读取、写入和更新操作的原子性,从而避免线程安全问题。以下是AtomicInteger
类中一些常用方法的含义:
int get()
- 获取当前的值。
void set(int newValue)
- 设置当前值为给定的值。
void lazySet(int newValue)
- 最终设置为给定值,但允许之后的其他内存操作重新排序(即不保证立即可见性给其他线程)。
boolean compareAndSet(int expect, int update)
- 如果当前值等于预期值,则以原子方式将该值设置为给定的更新值,如果更新成功,则返回
true
,否则返回false
。
- 如果当前值等于预期值,则以原子方式将该值设置为给定的更新值,如果更新成功,则返回
int getAndSet(int newValue)
- 以原子方式设置为给定值,并返回旧值。
int getAndIncrement()
- 以原子方式将当前值加1,并返回旧值。
int getAndDecrement()
- 以原子方式将当前值减1,并返回旧值。
int getAndAdd(int delta)
- 以原子方式将给定的值加到当前值,并返回旧值。
int incrementAndGet()
- 以原子方式将当前值加1,并返回新值。
int decrementAndGet()
- 以原子方式将当前值减1,并返回新值。
int addAndGet(int delta)
- 以原子方式将给定的值加到当前值,并返回新值。
int updateAndGet(IntUnaryOperator updateFunction)
- 使用给定的函数以原子方式更新当前值,并返回更新后的值,该函数接受当前值并计算一个新值。
boolean weakCompareAndSet(int expect, int update)
- 如果当前值等于预期值,则尝试以原子方式将该值设置为给定的更新值,这个方法可能会失败,即使当前值与预期值相等,因此它被称为“weak”,如果更新成功,则返回
true
,否则返回false
。
- 如果当前值等于预期值,则尝试以原子方式将该值设置为给定的更新值,这个方法可能会失败,即使当前值与预期值相等,因此它被称为“weak”,如果更新成功,则返回
int getAndUpdate(IntUnaryOperator updateFunction)
- 使用给定的函数以原子方式更新当前值,并返回旧值。该函数接受当前值并计算一个新值。
int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)
- 使用给定的累加函数和值以原子方式更新当前值,并返回旧值。该函数接受两个参数:一个是当前值,另一个是给定的值
x
,并计算一个新值。
- 使用给定的累加函数和值以原子方式更新当前值,并返回旧值。该函数接受两个参数:一个是当前值,另一个是给定的值
int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction)
- 使用给定的累加函数和值以原子方式更新当前值,并返回新值。该函数接受两个参数:一个是当前值,另一个是给定的值
x
,并计算一个新值。
- 使用给定的累加函数和值以原子方式更新当前值,并返回新值。该函数接受两个参数:一个是当前值,另一个是给定的值
AtomicInteger技术原理
AtomicInteger
类它用于实现整数的原子操作,在多线程环境中,原子操作可以确保数据的一致性和线程安全,AtomicInteger
通过硬件级别的原子操作(例如,通过compare-and-swap
即CAS操作)来实现这些保证。
实现原理
AtomicInteger
的实现基于以下几个关键概念:
- Unsafe类:
Unsafe
类是Java中的一个底层类,提供了硬件级别的原子操作,这个类通常不直接暴露给普通Java应用开发者使用,而是被内部类如AtomicInteger
所使用,Unsafe
类提供了如compareAndSwapInt
等方法,这些方法可以原子地更新内存中的值。 - volatile关键字:
AtomicInteger
中的值被声明为volatile
,这意味着这个值的读取和写入操作会从主内存中直接进行,而不是从线程的本地缓存中进行,这确保了所有线程都能看到最新的值。 - CAS(Compare-And-Swap)操作:CAS操作是一个原子操作,它包括三个操作数——内存位置(V)、预期原值(A)和新值(B),如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置的值更新为新值B,否则,处理器不做任何操作,无论哪种情况,它都会在CAS指令之前返回该位置的值,这一过程是原子的,也就是说在执行过程中不会被其他线程打断。
底层算法
AtomicInteger
类中的每个方法都使用了上述的一个或多个概念来实现其原子性,例如:
get()
方法:由于内部值被声明为volatile
,因此每次调用get()
都会直接从主内存中读取最新的值。set(int newValue)
方法:直接设置新值到内部volatile
变量,由于volatile
的特性,这个新值会立即被写入主内存,并且对所有线程可见。compareAndSet(int expect, int update)
方法:这个方法使用Unsafe
类的compareAndSwapInt
方法来实现CAS操作,如果当前值与期望值expect
相等,则更新为update
值,并返回true
;否则不做任何操作并返回false
。incrementAndGet()
和decrementAndGet()
方法:这些方法内部使用了一个循环,通过CAS操作尝试更新值,如果更新成功,则返回新值;如果更新失败(由于并发修改),则重新尝试直到成功为止,这是一种称为“自旋锁”的技术。addAndGet(int delta)
等方法:类似地,这些方法也使用CAS操作在循环中尝试更新值,直到成功为止,它们可能涉及更复杂的计算或转换函数,但基本原理是相同的。
伪代码实现
AtomicInteger
类提供了许多实用的工具方法,如int getAndIncrement()
、int getAndDecrement()
和int getAndSet(int newValue)
等,下面是AtomicInteger
类中一些核心方法的伪代码案例,用来帮助理解AtomicInteger
类,如下代码:
import sun.misc.Unsafe;
public class AtomicInteger {
// 使用Unsafe类获取对变量的偏移量,以便进行内存操作
private static final Unsafe unsafe = getUnsafe();
private static final long valueOffset;
static {
try {
// 获取value字段的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
// 状态变量,存储整数值
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
// 基于CAS的原子递增方法
public final int incrementAndGet() {
while (true) {
// 读取当前值
int current = get();
// 计算新值
int next = current + 1;
// 尝试原子地更新值,如果当前值未变,则更新成功并返回新值;否则循环重试
if (compareAndSet(current, next)) {
return next;
}
}
}
// CAS操作
private boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// 获取当前值
public final int get() {
return value;
}
// 其他如decrementAndGet、getAndIncrement等方法也类似实现...
}
上述代码中:
volatile
关键字确保了对value
变量的读写操作具有内存可见性。Unsafe
类提供了硬件级别的原子操作支持,例如compareAndSwapInt
方法,它是一个原子操作,比较对象内存中的某个位置的值是否为期望值(expect),如果是则更新为新的给定值(update)。
实际的AtomicInteger
类在JDK内部实现会更复杂,包括处理内存模型和并发优化等细节,但上述伪代码足以展现其核心思想,通过循环CAS的方式实现无锁的原子操作。
自我总结
AtomicInteger
类通过底层的硬件支持,确保在多线程环境下对整数的操作是线程安全的。
优点:
- 线程安全:AtomicInteger通过原子操作保证了并发访问时的数据一致性,避免了使用synchronized等重量级锁带来的性能开销。
- 性能高效:相比传统的同步机制,AtomicInteger提供了更高的吞吐量,因为它避免了线程间的阻塞和上下文切换。
缺点:
- 使用场景限制:AtomicInteger适用于简单的原子操作,但在复杂的并发场景中可能不足以应对所有的同步需求。
- 内存消耗:虽然AtomicInteger比使用锁的开销小,但它仍然比非原子的整数类型占用更多的内存。
- ABA问题:在使用CAS操作时,可能会遇到ABA问题,即一个值从A变成B又变回A,CAS会认为值没有发生变化,尽管中间可能经历了其他操作。
使用建议:
当需要在多线程环境中安全地更新整数时,推荐使用AtomicInteger
类,但在使用之前,应仔细评估其是否满足特定的同步需求,对于更复杂的并发控制,可能需要考虑使用更强大的同步机制,如ReentrantLock或CyclicBarrier等。
END!
END!
END!
往期回顾
精品文章
Java并发基础:concurrent Flow API全面解析
Java并发基础:CopyOnWriteArraySet全面解析
Java并发基础:ConcurrentSkipListMap全面解析