CAS(Compare And Set)
Java从JDK 1.5开始提供了java.util.concurrent.atomic包(以下简称Atomic包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。
悲观锁与乐观锁
悲观锁,认为数据发生并发冲突的概率很大,所以读操作之前就上锁。synchronized关键字,ReentrantLock 都是悲观锁的典型例子。
对于乐观锁,认为数据发生并发冲突的概率比较小,所以读操作之前不上锁。等到写操作的时候,再判断数据在此期间是否被其 他线程修改了。如果被其他线程修改了,就把数据重新读出来,重复该过程;如果没有被修改,就写回去。判断数据是否被修改,同时写 回新值,这两个操作要合成一个原子操作,也就是CAS(Compare And Set)。
原子更新基本类型类
使用原子的方式更新基本类型,Atomic包提供了以下3个类。
❑ AtomicBoolean:原子更新布尔类型。
❑ AtomicInteger:原子更新整型。
❑ AtomicLong:原子更新长整型。
以上3个类提供的方法类似以AtomicInteger为例进行讲解,AtomicInteger的常用方法如下。
➢ int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
➢ boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
➢ int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
➢ void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
➢ int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
1 2 3 4 5 6 7 | public class AtomicIntegerTest { static AtomicInteger ai = new AtomicInteger( 1 ); public static void main(String[] args) { System.out.println(ai.getAndIncrement()); System.out.println(ai.get()); } } |
Unsafe的CAS
CAS函数,其实是封装的Unsafe类中的一个native函 数,如下所示。
1 2 3 | public final boolean compareAndSet( int expect, int update) { return unsafe.compareAndSwapInt( this , valueOffset, expect, update); } |
AtomicInteger封装过的compareAndSet有两个参数。
第一个参数e xpect是指变量的旧值(是读出来的值,写回去的时候,希望没有被其 他线程修改,所以称为expect);
第二个参数update是指变量的新值 (修改过的,希望写入的值)。
当expect等于变量当前的值时,说明 在修改的期间,没有其他线程对此变量进行过修改,所以可以成功写 入,变量被更新为update,返回true;否则返回false。
Unsafe类是整个Concurrent包的基础,里面所有函数都是native 的。具体到compareAndSwapInt函数,如下所示。
1 | public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); |
该函数有4个参数。在前两个参数中,第一个是对象(也就是 AtomicInteger 对象),第二个是对象的成员变量(也就是AtomicInteger里面包的int变量value),后两个参数保持不变。 要特别说明一下第二个参数,它是一个long型的整数,经常被称为xxxOffset,意思是某个成员变量在对应的类中的内存偏移量(该变 量在内存中的位置),表示该成员变量本身。在Unsafe中专门有一个函数,把成员变量转化成偏移量,如下所示。
1 | public native long objectFieldOffset(Field var1); |
所有调用CAS的地方,都会先通过这个函数把成员变量转换成一个 Offset。以AtomicInteger为例:
从上面代码可以看到,无论是Unsafe还是valueOffset,都是静态 的,也就是类级别的,所有对象共用的。 在转化的时候,先通过反射(getDeclaredField)获取value成员 变量对应的Field对象,再通过objectFieldOffset函数转化成valueOffset。此处的valueOffset就代表了value变量本身,后面执行CAS操作 的时候,不是直接操作value,而是操作valueOffset。
原子更新数组
通过原子的方式更新数组里的某个元素,Atomic包提供了以下3个类。
❑ AtomicIntegerArray:原子更新整型数组里的元素。
❑ AtomicLongArray:原子更新长整型数组里的元素。
❑ AtomicReferenceArray:原子更新引用类型数组里的元素。
注意,这里并不是说对整个数组的操作是原子的,而是针对数组中一个元素的原子操作而言。
AtomicIntegerArray类常用方法如下。
➢int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。
➢boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。
1 2 3 4 5 6 7 8 9 | public class AtomicIntegerArrayTest { static int [] value = new int []{ 1 , 2 }; static AtomicIntegerArray ai = new AtomicIntegerArray(value); public static void main(String[] args) { ai.getAndSet( 0 , 3 ); System.out.println(ai.get( 0 )); System.out.println(value[ 0 ]); } } |
需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。
原子更新引用类型
原子更新引用类型提供的类。Atomic包提供了以下3个类。
❑ AtomicReference:原子更新引用类型。
❑ AtomicReferenceFieldUpdater:原子更新引用类型里的字段。
❑ AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef, boolean initialMark)。
以上几个类方法类似,以AtomicReference为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class AtomicReferenceTest { public static AtomicReference<User> atomicUserRef = new AtomicReference<User>(); public static void main(String[] args) { User user = new User( "conan" , 15 ); atomicUserRef.set(user); User updateUser = new User( "Shinichi" , 17 ); atomicUserRef.compareAndSet(user, updateUser); System.out.println(atomicUserRef.get().getName()); System.out.println(atomicUserRef.get().getOld()); } public static class User { private String name; private int old; public User(String name, int old) { this .name = name; this .old = old; } public String getName() { return name; } public int getOld() { return old; } } } |
原子更新字段类
原子更新字段类,Atomic包提供了以下3个类进行原子字段更新。
❑ AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
❑ AtomicLongFieldUpdater:原子更新长整型字段的更新器。
❑ AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。
要想原子地更新字段类需要两步。
第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
第二步,更新类的字段(属性)必须使用public volatile修饰符。
以上3个类提供的方法类似,以AstomicIntegerFieldUpdater为例,AstomicIntegerFieldUpdater的示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class AtomicIntegerFieldUpdaterTest { private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User. class , "old" ); public static void main(String[] args) { User conan = new User( "conan" , 10 ); System.out.println(a.getAndIncrement(conan)); System.out.println(a.get(conan)); } public static class User { private String name; public volatile int old; public User(String name, int old) { this .name = name; this .old = old; } public String getName() { return name; } public int getOld() { return old; } } } |
参考: Java并发编程的艺术 7.1 原子更新基本类型类
Java并发实现原理:Atomic类
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
2018-01-25 js判断一个字符串是以某个字符串开头
2018-01-25 如何 通过 userAgent 区别微信小程序