早期的JDK版本中,如果要并发的对Integer、Long、Double之类的Java原始类型或引用类型进行操作,一般都需要通过锁来控制并发,以防数据不一致。
从JDK1.5开始,引入了java.util.concurrent.atomic 工具包,该包提供了许多Java原始/引用类型的映射类,如AtomicInteger 、AtomicLong 、AtomicBoolean ,这些类可以通过一种“无锁算法”,线程安全的操作Integer、Long、Boolean等原始类型。
所谓“无锁算法”,我们在讲juc-locks锁框架系列中,已经接触过太多次了,其实底层就是通过Unsafe类实现的一种比较并交换的算法CAS,大致的结构如下(具体入参,根据上下文有所不同): boolean compareAndSet(expectedValue, updateValue); 当希望修改的值与expectedValue相同时,则尝试将值更新为updateValue,更新成功返回true,否则返回false。
java.util.concurrent.atomic 包结构如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-THIXfHxF-1643903610412)(C:\Users\14470\Desktop\java笔记\java JUC\J.U.C之atomic框架.assets\image-20220203210529126.png)]
2. AtomicInteger的使用
AtomicInteger,应该是atomic框架中用得最多的原子类了。顾名思义,AtomicInteger是Integer类型的线程安全原子类,可以在应用程序中以原子的方式更新int值。
来看下面这个示例程序:
public class Main {
public static void main(String[] args) throws InterruptedException {
AtomicInteger ai = new AtomicInteger();
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(new Accumlator(ai), "thread-" + i);
list.add(t);
t.start();
}
for (Thread t : list) {
t.join();
}
System.out.println(ai.get());
}
static class Accumlator implements Runnable {
private AtomicInteger ai;
Accumlator(AtomicInteger ai) {
this.ai = ai;
}
@Override
public void run() {
for (int i = 0, len = 1000; i < len; i++) {
ai.incrementAndGet();
}
}
}
}
结果:10000
可以看出我们循环创建了10个线程对AtomicInteger变量ai.incrementAndGet(); 操作。
即以原子的操作对int值进行自增。如果不使用AtomicInteger,使用原始的int或Integer,最终结果值可能会小于10000(并发时读到了过时的数据或存在值覆盖的问题)。
它里面有很多类似的方法,大部分都是基于Unsafe 类实现的,实现了原子操作。
(Unsafe类详解可见我的另一篇文章JAVA中的Unsafe类看着一篇就够了。CAS操作原理详见:并发编程的基石CAS
源码解读
它有了两种构造方法
有参构造器
public AtomicInteger(int initialValue)
用给定的初始值创建一个新的AtomicInteger。
无参构造器
public AtomicInteger()
创建一个新的AtomicInteger,初始值为 0 。
源码摘录:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
AtomicInteger 中大部分操作都依靠Unsafe类完成,所以他一上来就直接Unsafe.getUnsafe() 获取Unsafe 实例。
valueOffset 这个是指类中相应字段在该类的偏移量,在这里具体即是指value这个字段在AtomicInteger类的内存中相对于该类首地址的偏移量。
`一个对象一创建,然后给他分配内存空间,这样他就有一个开始的地址,比如是000100。然后对象里面有一堆属性,他们也需要空间呀。 那么他们就在 地址000100 后面挨个分配好自己的空间,比如参数A就分配到了000111地址。 那么valueOffset方法就可以获取到地址偏移量:000111-000100=000011 后面我们就可以用一些方法用地址偏移量从起始地址出发获取到这个参数A了。
然后可以看一个有一个静态初始化块,这个块的作用即是求出value 这个字段的偏移量。具体的方法使用的反射的机制得到value 的Field 对象,再根据objectFieldOffset 这个方法求出value 这个变量内存中在该对象中的偏移量。
因为其中的方法大同小异,基本都是依靠Unsafe的CAS操作方法来实现的,所以这里就列举两个方法来讲解。
方法一:AtomicInteger.incrementAndGet()
看源码:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
他onlyonlyonly调用了unsafe的getAndAddInt() 方法。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2)
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
getIntVolatile 方法用于在对象指定偏移地址处volatile 读取一个int 。volatile 读写可以保证可见性和有序性。(详见Unsafe 和并发编程的基石CAS)
方法二:AtomicInteger的特殊方法
AtomicInteger中有一个比较特殊的方法——lazySet:
看源码
private volatile int value;
public final void set(int newValue) {
value = newValue;
}
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
可以看到value 是用volatile 修饰的。我们知道通过volatile修饰的变量,可以保证在多处理器环境下的“可见性”。也就是说当一个线程修改一个共享变量时,其它线程能立即读到这个修改的值。volatile的实现最终是加了内存屏障:
- 保证写volatile变量会强制把CPU写缓存区的数据刷新到内存
- 读volatile变量时,使缓存失效,强制从内存中读取最新的值
- 由于内存屏障的存在,volatile变量还能阻止重排序
lazySet内部调用了Unsafe类的putOrderedInt方法,通过该方法对共享变量值的改变,不一定能被其他线程立即看到。也就是说以普通变量的操作方式来写变量。
为什么会有这种奇怪方法?什么情况下需要使用lazySet呢?
考虑下面这样一个场景:
private AtomicInteger ai = new AtomicInteger();
lock.lock();
try
{
}
finally
{
lock.unlock();
}
由于锁的存在:
- **lock()**方法获取锁时,和volatile变量的读操作一样,会强制使CPU缓存失效,强制从内存读取变量。
- **unlock()**方法释放锁时,和volatile变量的写操作一样,会强制刷新CPU写缓冲区,把缓存数据写到主内存
所以,上述ai.set(1) 可以用ai.lazySet(1) 方法替换:
由锁来保证共享变量的可见性,以设置普通变量的方式来修改共享变量,减少不必要的内存屏障,从而提高程序执行的效率。
再再其他方法:
三、其它原子类
与AtomicInteger类似的原子类还有AtomicBoolean和AtomicLong,底层都是通过Unsafe类做CAS操作,来原子的更新状态值。 相信大家伙一定可以举一反三。
|