原子类、CAS、Unsafe
一、什么是CAS
CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。
简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。
相信sql大家都熟悉,类似sql中的条件更新一样:update set id=3 from table where id=2。因为单条sql执行具有原子性,如果有多个线程同时执行此sql语句,只有一条能更新成功。
备注:常见的自增操作不能用volatile来实现?这是因为volatile只能保证可见性,无法保证原子性,而自增操作并不是一个原子操作。
CAS底层大致实现思路:
我们都知道,在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量直至并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。
调用汇编层“Atomic::cmpxchg”方法,判断是否为多处理器,通过LOCK_IF_MP(mp)会根据mp的值来决定是否为cmpxchg指令添加lock前缀,这是一种优化手段,认为单处理器的环境没有必要添加lock前缀,只有在多核情况下才会添加lock前缀,因为lock会导致性能下降。cmpxchg是汇编指令,作用是比较并交换操作数。
intel手册对lock前缀的说明如下:
- 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
- 禁止该指令与之前和之后的读和写指令重排序。
- 把写缓冲区中的所有数据刷新到内存中。
上面的第1点保证了CAS操作是一个原子操作,第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。
CAS如何保证原子性:处理器自动保证基本内存操作的原子性,第一种使用总线锁保证原子性,第二种使用缓存保证原子性。
CAS的缺点:
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
- 循环时间长开销很大。例如getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。但是,如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行命令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率
- 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
- ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
二、UnSafe类详解:
Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。
这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。
Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,UnSafe类总体功能:

Unsafe与CAS
从源码中发现,内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)。
public final int getAndAddInt(Object paramObject, long paramLong, int paramInt) { int i; do i = getIntVolatile(paramObject, paramLong); while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt)); return i; }
又从Unsafe类中发现,原子操作其实只支持下面三个方法。
Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong。都是native方法。
Unsafe底层
public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3); public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2); public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);
Unsafe其它功能
public native long staticFieldOffset(Field paramField); //个方法可以用来获取给定的 paramField 的内存地址偏移量,这个值对于给定的 field 是唯一的且是固定不变的。 public native int arrayBaseOffset(Class paramClass); public native int arrayIndexScale(Class paramClass); //前一个方法是用来获取数组第一个元素的偏移地址,后一个方法是用来获取数组的转换因子即数组中元素的增量地址的。 public native long allocateMemory(long paramLong); public native long reallocateMemory(long paramLong1, long paramLong2); public native void freeMemory(long paramLon //分别用来分配内存,扩充内存和释放内存的。
三、原子类
JDK中提供了13个原子操作类。
1、原子更新基本类型
使用原子的方式更新基本类型,Atomic包提供了以下3个类。以下3个类提供的方法几乎一模一样
- AtomicBoolean: 原子更新布尔类型。
- AtomicInteger: 原子更新整型。
- AtomicLong: 原子更新长整型。
详细看下AtomicInteger类,常用方法如下:
public final int get():获取当前的值 public final int getAndSet(int newValue):获取当前的值,并设置新的值 public final int getAndIncrement():获取当前的值,并自增 public final int getAndDecrement():获取当前的值,并自减 public final int getAndAdd(int delta):获取当前的值,并加上预期的值 void lazySet(int newValue): 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
AtomicInteger 底层用的是volatile的变量和CAS来进行更改数据的。源码如下:
- volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值
- CAS 保证数据更新的原子性。
public class AtomicInteger extends Number implements java.io.Serializable { private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { //用于获取value字段相对当前对象的“起始地址”的偏移量 valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; //返回当前值 public final int get() { return value; } //递增加detla public final int getAndAdd(int delta) { //三个参数,1、当前的实例 2、value实例变量的偏移量 3、当前value要加上的数(value+delta)。 return unsafe.getAndAddInt(this, valueOffset, delta); } //递增加1 public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } ... }
2、原子更新数组
通过原子的方式更新数组里的某个元素,Atomic包提供了以下的4个类:
- AtomicIntegerArray: 原子更新整型数组里的元素。
- AtomicLongArray: 原子更新长整型数组里的元素。
- AtomicReferenceArray: 原子更新引用类型数组里的元素。 这三个类的最常用的方法是如下两个方法:
- get(int index):获取索引为index的元素值。
- compareAndSet(int i,E expect,E update): 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值。
使用示例如下:
import java.util.concurrent.atomic.AtomicIntegerArray; public class Demo5 { public static void main(String[] args) throws InterruptedException { AtomicIntegerArray array = new AtomicIntegerArray(new int[] { 0, 0 }); System.out.println(array); System.out.println(array.getAndAdd(1, 2)); System.out.println(array); } }
3、原子更新引用类型
Atomic包提供了以下三个类:
- AtomicReference: 原子更新引用类型。
- AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。
- AtomicMarkableReferce: 原子更新带有标记位的引用类型。
这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象set进Atomic类,然后调用compareAndSet等一些方法去进行原子操作,原理都是基于Unsafe实现,但AtomicReferenceFieldUpdater略有不同,更新的字段必须用volatile修饰。
import java.util.concurrent.atomic.AtomicReference; public class AtomicReferenceTest { public static void main(String[] args){ // 创建两个Person对象,它们的id分别是101和102。 Person p1 = new Person(101); Person p2 = new Person(102); // 新建AtomicReference对象,初始化它的值为p1对象 AtomicReference ar = new AtomicReference(p1); // 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。 ar.compareAndSet(p1, p2); Person p3 = (Person)ar.get(); System.out.println("p3 is "+p3); System.out.println("p3.equals(p1)="+p3.equals(p1)); } } class Person { volatile long id; public Person(long id) { this.id = id; } public String toString() { return "id:"+id; } }
结果说明:
- 新建AtomicReference对象ar时,将它初始化为p1。
- 紧接着,通过CAS函数对它进行设置。如果ar的值为p1的话,则将其设置为p2。
- 最后,获取ar对应的对象,并打印结果。p3.equals(p1)的结果为false,这是因为Person并没有覆盖equals()方法,而是采用继承自Object.java的equals()方法;而Object.java中的equals()实际上是调用"=="去比较两个对象,即比较两个对象的地址是否相等。
4、原子更新字段类
Atomic包提供了四个类进行原子字段更新:
- AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
- AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
- AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
- AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述。
这四个类的使用方式都差不多,是基于反射的原子更新字段的值。要想原子地更新字段类需要两步:
- 第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
- 第二步,更新类的字段必须使用public volatile修饰。
再说下对于AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束,约束如下: 字段必须是volatile类型的,在线程之间共享变量时保证立即可见.eg:volatile int value = 3
字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。
但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。 只能是实例变量,不能是类变量,也就是说不能加static关键字。 只能是可修改变量,不能使final变量,
因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,
不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。
public class TestAtomicIntegerFieldUpdater { public static void main(String[] args){ TestAtomicIntegerFieldUpdater tIA = new TestAtomicIntegerFieldUpdater(); tIA.doIt(); } public AtomicIntegerFieldUpdater<DataDemo> updater(String name){ return AtomicIntegerFieldUpdater.newUpdater(DataDemo.class,name); } public void doIt(){ DataDemo data = new DataDemo(); System.out.println("publicVar = "+updater("publicVar").getAndAdd(data, 2)); /* * 由于在DataDemo类中属性value2/value3,在TestAtomicIntegerFieldUpdater中不能访问 * */ //System.out.println("protectedVar = "+updater("protectedVar").getAndAdd(data,2)); //System.out.println("privateVar = "+updater("privateVar").getAndAdd(data,2)); //System.out.println("staticVar = "+updater("staticVar").getAndIncrement(data));//报java.lang.IllegalArgumentException /* * 下面报异常:must be integer * */ //System.out.println("integerVar = "+updater("integerVar").getAndIncrement(data)); //System.out.println("longVar = "+updater("longVar").getAndIncrement(data)); } } class DataDemo{ public volatile int publicVar=3; protected volatile int protectedVar=4; private volatile int privateVar=5; public volatile static int staticVar = 10; //public final int finalVar = 11; public volatile Integer integerVar = 19; public volatile Long longVar = 18L; }
AtomicStampedReference如何解决ABA问题
AtomicStampedReference主要维护包含一个对象引用以及一个可以自动更新的整数"stamp"的pair对象来解决ABA问题。
-
如果元素值和版本号都没有变化,并且和新的也相同,返回true;
-
如果元素值和版本号都没有变化,并且和新的不完全相同,就构造一个新的Pair对象并执行CAS更新pair。
可以看到,java中的实现跟我们上面讲的ABA的解决方法是一致的。
-
首先,使用版本号控制;
-
其次,不重复使用节点(Pair)的引用,每次都新建一个新的Pair来作为CAS比较的对象,而不是复用旧的;
-
最后,外部传入元素值及版本号,而不是节点(Pair)的引用
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; .... /** * expectedReference :更新之前的原始值 * newReference : 将要更新的新值 * expectedStamp : 期待更新的标志版本 * newStamp : 将要更新的标志版本 */ 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) || // 构造新的Pair对象并CAS更新 casPair(current, Pair.of(newReference, newStamp))); } private boolean casPair(Pair<V> cmp, Pair<V> val) { // 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用 return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); }
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(1, 0); public static void main(String[] args){ Thread main = new Thread(() -> { System.out.println("操作线程" + Thread.currentThread() +",初始值 a = " + atomicStampedRef.getReference()); int stamp = atomicStampedRef.getStamp(); //获取当前标识别 try { Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行 } catch (InterruptedException e) { e.printStackTrace(); } boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1); //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败 System.out.println("操作线程" + Thread.currentThread() +",CAS操作结果: " + isCASSuccess); },"主操作线程"); Thread other = new Thread(() -> { Thread.yield(); // 确保thread-main 优先执行 atomicStampedRef.compareAndSet(1,2,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1); System.out.println("操作线程" + Thread.currentThread() +",【increment】 ,值 = "+ atomicStampedRef.getReference()); atomicStampedRef.compareAndSet(2,1,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1); System.out.println("操作线程" + Thread.currentThread() +",【decrement】 ,值 = "+ atomicStampedRef.getReference()); },"干扰线程"); main.start(); other.start(); } 输出结果:
// 输出 > 操作线程Thread[主操作线程,5,main],初始值 a = 2 > 操作线程Thread[干扰线程,5,main],【increment】 ,值 = 2 > 操作线程Thread[干扰线程,5,main],【decrement】 ,值 = 1 > 操作线程Thread[主操作线程,5,main],CAS操作结果: false
java中还有哪些类可以解决ABA的问题?
AtomicMarkableReference,它不是维护一个版本号,而是维护一个boolean类型的标记,标记值有修改,了解一下。
主要参考链接:https://www.pdai.tech/md/java/thread/java-thread-x-juc-AtomicInteger.html
浙公网安备 33010602011771号