原子类型的意义
对于一个共享变量,在多线程的情况下,我们需要对其进行同步操作,最常用的手段是添加synchronized关键字。如
public class SharedValue {
private int value;
public synchronized void add(int count){
this.value += count;
}
public synchronized void sub(int count) {
this.value -= count;
}
}
但是,虽然synchronized关键字经过Java团队多年的优化(通过偏向锁->轻量级锁->重量级锁),其仍然存在一些性能问题,并且在开发中需要显示的添加synchronized关键字,容易遗漏某些方法。因此JDK团队在concurrent包中封装了很多的原子类型,底层通过CPU硬件级别的CAS锁保障了线程安全性。
2. 乐观锁和悲观锁
- 乐观锁:认为数据发生并发冲突的概率比较小,所以读操作之前不加锁。等到写操作的时候,再判断数据在此期间是否被其他线程修改了。如果被其他线程修改了,就把数据重新读出来,重复该过程。如果没有被修改,就写回去。判断数据是否被修改,同时还要写回新的值,这两个操作要合成一个原子操作,就是通过CPU提供的CAS实现的
- 悲观锁:任务数据发生并发冲突的概率很大,所以读操作之前就上锁。
- Unsafe类的CAS操作
上述的CAS操作,被JDK封装成了Unsafe类的一个native方法,供上层应用程序调用,如AtomicInteger
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
Unsafe类是整个Concurrent包的基础,里面所有函数都是native的,如上述的
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5)方法
- 第一个参数:是一个对象,表示的就是就是调用方对象。
- 第二个参数:需要特别说明,它是一个long型的整数,经常被称为xxxOffset,意思是某个成员变量在对应的类中的内存偏移量(该变量在内存中的地址),表示该成员变量本身。Unsafe有一个专门的函数,把成员变量转化为偏移量
public native long objectFieldOffset(Field var1);。- 第三个参数:期望值,即希望xxxOffset处的变量的当前值,相等则更新该值为第四个参数,并返回true,不相等则返回false
- 第四个参数:新值,即等待被插入的值,插入不成功时,需通过”自旋“重新读取except(第三个参数)。
获取成员变量偏移量,即第二参数:如AtomicInteger中的static代码块
static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
- 自旋与阻塞
当一个线程拿不到锁的时候,有以下两种基本的等待策略。
- 策略1:放弃CPU,进入阻塞状态,等待后续被唤醒,再重新被操作系统调度。也就是“阻塞”
- 策略2:不放弃CPU,空转,不断重试。也就是所谓的”自旋“
注意:这两种策略并不是互斥的,可以结合使用。如果拿不到锁,先自旋几次;仍然拿不到锁时,进入阻塞。synchronized关键字就是这样实现的。
AtomicInteger&AtomicLong&AtomicReference
- AtomicInteger和AtomicLong都继承了Number抽象类,并且内部包装了一个value值(int类型或者long类型或者Object泛型类型),需要注意的是,value是
volatile类型的。因此会保证多线程下,value值的可见性和有序性,避免重排序。
public class AtomicInteger extends Number implements java.io.Serializable {
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); }
}
private volatile int value;
......
}
- 初始化一个Unsafe对象,后续的所有原子操作,都是通过Unsafe中的native方法完成的。
- 在静态代码块中初始化value的偏移量,后续unsafe操作中需要作为参数传入。
- 定义一个对应类型的volatile变量。作为共享变量。
getAndIncrement以及incrementAndGet等方法都是都是通过unsafe.getAndAddXxx()完成,getAndSet是通过unsafe.getAndSet()方法完成。
AtomicBoolean
- 由于unsafe原生不支持对boolean的CAS操作,只提供了三种类型的CAS操作:int,long,Object,因此底层会将boolean类型,转换为int类型处理,true转换为1,false转换为0.
private volatile int value;
......
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
AtomicStampedReference&AtomicMarkableReference
- CAS引入的问题,即ABA问题:如果当前线程X执行compareAndSet方法时(此时已经取得了expect值A),另一个线程Y获取到CPU时间片,并且把变量的值从A改为B,然后再从B改到A,那么尽管修改过两次,可是当前线程做CAS操作的时候,却会因为值没变而认为数据没有被其他线程修改过。可以正常完成
compareAndSet()操作。 AtomicStampedReference如何解决ABA问题:比较时,不仅要比较“值”,而且提供一个版本号“stamp”,还要比较版本号是否一致。
- 提供了一个内部类
Pair,封装了值value和版本号stamp.同时将Pair对象作为底层Unsafe的操作对象。public class AtomicStampedReference<V> { private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } } private volatile Pair<V> pair; ...... }
- 执行比较操作时,CAS操作
compareAndSet()方法有四个参数,后面两个参数就是版本号的旧值和新值。
- 当expectedReference != 对象当前的reference时,说明该数据肯定被其他线程修改过;直接返回false
当expectedReference==对象当前的reference时,再进一步比较expectedStamp是否等于对象当前的版本号,以此判断数据是否被其他线程修改过。public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); } private boolean casPair(Pair<V> cmp, Pair<V> val) { return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); }
- 为什么没有AtomicStampedInteger或者AtomicStampedLong
因为这里要同时比较数据的“值”和“版本号”,而Integer或者Long的CAS没有办法同时比较两个变量,于是只能把值和版本号封装成一个对象。
扩展:后续章节将读写锁的时候,会提到JDK在实现读写锁时通过将一个int类型的CAS操作拆分成前16位和后16位,从而打完一次CAS完成多个操作的目的。
- AtomicMarkableReference:实现原理同AtomicStampedReference,不过将int类型的stamp版本号变成了一个boolean类型的标记。
此处需要注意的是:因为boolean类型的取值只有true和false两个版本号,所以并不能完全避免ABA问题,只是降低了ABA发生的概率。
本文探讨了在多线程环境下,通过原子类型和CAS操作确保数据一致性的重要性。介绍了乐观锁与悲观锁的概念,深入剖析了Unsafe类的CAS操作及自旋与阻塞策略。详细解释了AtomicInteger、AtomicLong、AtomicReference等原子类的工作原理,以及AtomicStampedReference如何解决ABA问题。
791

被折叠的 条评论
为什么被折叠?



