在之前我们提过CAS这个玩意,这也是个很重要的玩意,在JDK中, 有很多地方都用到了它
CAS基础了解
CAS:compare and swap,比较与交换
通常指的是这样的一种原子操作:针对一个变量,先看它的内存值与某个期望值是否相同,相同就给它重新赋值
CAS用伪代码可以表示为这样:
if (value == 期望值) {
value = 新值;
}
这个伪代码比较和赋值这两步走的,CAS可以看做是他们的合成体,CAS是在硬件层面进行原子性保证的CAS可以当做是乐观锁的实现方式,这块可以和数据库的悲观锁和乐观锁作对比
CAS的应用
Java中原子类的增加变更操作就是通过CAS自旋实现的
CAS是一种无锁算法,在不使用锁,也就是线程不被阻塞的情况下,实现多线程之间的变量同步
在Java中,CAS操作是通过Unsafe类提供的,该类提供了三种方式
他们都是 native,本地方法,由JVM提供具体实现,这就意味着JVM对它们的实现可能不同
以 compareAndSwapInt 为例,Unsafe 的 compareAndSwapInt 方法接收 4 个参数,分别是:对象实例、内存偏移量、字段期望值、字段新值,我们来写一段代码测试一下
public static void main(String[] args) throws Exception {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
User user = new User();
user.setId(1);
long offect = unsafe.objectFieldOffset(user.getClass().getDeclaredField("id"));
System.out.println("unsafe.compareAndSwapInt(user, offect, 1, 2) = " + unsafe.compareAndSwapInt(user, offect, 1, 2));
System.out.println("unsafe.compareAndSwapInt(user, offect, 3, 6) = " + unsafe.compareAndSwapInt(user, offect, 3, 6));
System.out.println("unsafe.compareAndSwapInt(user, offect, 2, 21) = " + unsafe.compareAndSwapInt(user, offect, 2, 21));
}
我这个User实体类,有一个属性是 int 类型的,属性名为id
这儿要注意一个东西,Unsafe不能直接创建,不信咱试试
Unsafe unsafe = Unsafe.getUnsafe();
都说了 Unsafe,Unsafe,怎么可能让随便掉用,这玩意可是能直接操作内存的,看它的get方法
可以看出,它先获取了当前类的类加载器,进行了一个判断 VM.isSystemDomainLoader
点进来发现只是判断是不是为null,所以关键点还是在获取类加载器上,什么情况下返回null,什么情况下不返回null
@CallerSensitive
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}
可以看到,上来先获取了ClassLoader,获取了类加载器,如果为null,那就直接返回,这儿如果返回了null,那上面的校验直接是false,获取Unsafe就会抛出异常
如果获取到的类加载器不是null,它取获取了系统安全管理器,如果系统安全管理器不为空,那就执行 ClassLoader 的 checkClassLoaderPermission ,检查类加载器许可
static void checkClassLoaderPermission(ClassLoader cl, Class<?> caller) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// caller can be null if the VM is requesting it
ClassLoader ccl = getClassLoader(caller);
if (needsClassLoaderPermissionCheck(ccl, cl)) {
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
这个方法有两个入参,第一个可以看到是获取的类加载器,第二个是什么呢?
从上层可以看到,是 Reflection.getCallerClass(),这个方法,可以获取到调用者的类
可以看到,它又获取了调用者的类加载器,在 if 里,进行了一个类加载器许可检查
注意,前面是调用者,后边是当前,前面没什么,关键看最后这个,它对调用者进行了判断
其实这块就是在判断 var0是不是BootstrapClassLoader,因为双亲委派机制的保护,不是的话,直接抛异常出去
扩展,类加载器:
- BootstrapClassLoader(启动类加载器):主要负责加载核心的类库(java.lang.*等),JVM_HOME/lib目录下的,构造ExtClassLoader和APPClassLoader。
- ExtClassLoader (拓展类加载器):主要负责加载jre/lib/ext目录下的一些扩展的jar
- AppletClassLoader(系统类加载器):主要负责加载应用程序的主函数类
- 自定义类加载器:主要负责加载应用程序的主函数类
当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载
好了, 让我们再回到CAS上,画个图来做示例
注意,真正的CAS,能且只能保证原子性,Java中的CAS,JVM做了优化, 可以保证线程安全,在Java中实现,其实也很简单,在CAS前面添加LOCK#前缀指令即可
上面我们在用CAS的时候,有一个偏移量,这个我们注意一下,子类初始化时,会先初始化父类,如果父类还有父类,则继续初始化,最终会到Object(这也是为什么子类可以强转到父类,因为子类内存空间含有父类的那一块初始化好的内存)
在一个对象初始化后,它有一个对象头,64 位的 Java 虚拟机中,对象头的标记字段占 64 位,而类型指针又占了 64 位,也就是说,一个Java对象对内存的占用就是16个字节,为了减少开销,64 位 Java 虚拟机引入了压缩指针,将堆中原本 64 位的 Java 对象指针压缩成 32 位的,也就是说,从原来的16个字节占用, 压缩到了12个字节占用,但是,虚拟机要求对象起始地址必须是8字节的整数倍,所以必须填充4个字节,到16字节,填充数据并非必须存在, 仅仅是为了字节对齐
我们在Java使用CAS,那么在虚拟机,一定有它的实现,感兴趣的可以看JDK的源码实现
不过,不同的操作系统,不同的CPU,都会有不同的实现,无论是虚拟机的CAS实现,还是Java中的CompareAndSwap,都是对相应平台的CAS指令的一层简单封装,CAS作为一种硬件原语,有着天然的原子性,这也是CAS的价值所在
CAS的缺陷
为了进行更改,可能会进行自旋+CAS,长时间的自旋CAS操作不成功,会给CPU带来很大的开销
且,CAS只能保证一个共享变量原子操作
还有就是ABA问题,所谓ABA问题,也很简单,我们举个例子,thread1令value=1,thread2令value=2,再令value=1,thread1再读,还是1,看似没有变化,实际上已经发生了变化
ABA问题的解决
要解决ABA问题,其实也很简单,像数据库的乐观锁,搞一个版本号或者标记
Java提供了一个原子引用类,AtomicStampedReference<V>,它有两个属性,reference-实际存储的变量,int类型属性 stamp-版本,修改一次就加一
还有一个简化版的类,AtomicMarkableReference<V>,它有两个属性,reference-实际存储的变量,boolean类型属性 mark-是否修改过
CAS原子操作类
我们知道,在并发情况下,很容易出现数据安全问题,比如多个线程执行 ++ 操作,就有可能是错误的结果,为了保证结果,通常可以使用synchronized来控制,但是synchronized是悲观锁,并不是很高效
Java为了保证原子性,提供了Atomic....相关的类,atomic包下都是采用乐观锁策略去原子更新数据的,Java中,使用了CAS进行具体实现
atomic包下,有五大类
1、基本类型:AtomicInteger,AtomicLong,AtomicBoolean
2、引用类型:AtomicStampedReference,AtomicMarkableReference
3、数组类型:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
4、对象属性原子修改器:AtomicIntegerFieldUpdate,AtomicLongFieldUpdate,AtomicReferenceFieldUpdate
5、原子类型累加器:LongAccumulator,DoubleAccumulator,DoubleAdder,LongAdder
这些类具体的使用我们不做赘述,各位看官可自行研究相关API
这里有一个地方要注意,AtomicIntegerFieldUpdate的使用是存在限制的
1、字段必须用volatile修饰,在线程之间共享变量保证立刻可见
2、字段的描述类型(public、protected、default、private)与调用者与操作对象的关系一致,也就是说,调用者可以直接操作对象字段,那么,就可以反射进行原子操作,但是对于父类字段,子类不能直接操作,尽管,子类可以访问父类的字段
3、只能是实例变量,不能是类变量,也就是说不能加static
4、只能是可修改的变量,不能是final(事实上,final和volatile是冲突的,不能同时存在)
5、对于AtomicIntegerFieldUpdate或者AtomicLongFieldUpdate,只能修改int类型和long类型,不能改Integer和Long,如果要改的话,得用AtomicRenferenceUpdate(这一点也很好理解,Integer和Long,是对象,不是基本类型)
思考一个问题,无论是AtomicInteger还是AtomicLong,都有CAS的共同缺陷,并发高的时候,经常会失败自旋,贼影响性能,那该怎么办呢?
所以,Java也有改进,提供了DoubleAdder和LongAdder
原理,其实就是拆分处理,比如,十个线程进行累加,放在之前,肯定是同一时间点,有九个线程在自旋,现在我们把它拆开,十个线程自己加自己的,最后把这十个线程加在一起就好了
现在假设有四个线程访问一个变量,对变量进行++操作,线程1CAS成功,直接在变量上进行增加,但是其他三个线程都CAS失败了,这个时候,这三个CAS失败的三个线程,会自己处理自己后续的++操作请求,等全部都处理完了,这三个线程各自的处理结果,再和第一个处理结果相加,就是最后的结果
---- 我们以DoubleAdder为例
public void add(double x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null ||
!casBase(b = base,
Double.doubleToRawLongBits
(Double.longBitsToDouble(b) + x))) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value,
Double.doubleToRawLongBits
(Double.longBitsToDouble(v) + x))))
doubleAccumulate(x, null, uncontended);
}
}
可以看到,进行了一个判断,这个as上来一定是空的,我们可以去看看这个cells是什么
这个是DoubbleAdder父类,我们再去追,发现它是一个内部类,这个内部类还考虑到了伪共享的问题,什么是伪共享,请看我这篇文章最后一个节
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
所以前面是false,那就是看后边的了
很明显,后边用CAS在操作base,base是什么呢,那就是要操作的原始值,假设第一个线程进来, CAS修改一定是成功的,两边都是false,那就正常走,后边的线程如果失败了,那就会走这个if里的逻辑,就是这样滴~
我们继续来看,首先可以看到,里面的if,还是进行了Cell的判断,是不是进行了初始化
在这一步结束了之后,可以看到它执行了 a = as[getProbe() & m]
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
看,传了一个当前线程,这个就是在计算Cell下标,如果,这个下表为止不为空,看,有一个很重要的地方
是a的值,也就是当前下标的值,进行一个加操作,而不是在base上进行加
如果说,它也没成功的话,那就得执行最下面的 doubleAccumulate 方法了
点进去的话,会发现这个方法是父类 Striped64 提供的
上来之后,最明显的,就是一个自旋(叫死循环也没问题),一般进行CAS的都会有这种处理,因为要保证成功
我们来仔细看看这个方法的处理逻辑,前面就不用管了,我们直接看自旋了里的这一大堆判断,大家可以对着代码来看
首先,上来判断 cells 不为空,紧接着判断这个cell位置是不是为空,为空说明这个位置的cells对象还没创建出来
然后判断cellBusy是不是等于0,等于0的话,创建出来一个cell
注意这个 x ,这个 x 就是我们需要加的值,所以说,假设cell刚开始没有,要进行加一,那就是一,后续的加,就在这个一的基础上加
在这个时候cell已经创建出来了,但是,小心但是!
但是,创建的cell还没有放到cells里,所以需要给它放进去,我们看它的处理
if (cellsBusy == 0 && casCellsBusy())
我们看这个if,它判断了 cellsBusy是不是等于0,并且,用CAS,尝试改成1,改成功了,其实说明当前线程获取到锁了
剩下的里面的逻辑,其实就是在创建,赋值,然后最后把cessBusy改成0
注意,到这儿,我们的前提都是,为空的情况下,那,如果不为空呢?
让我们直接跳过这个if,看下面,如果cell不为空怎么办
第一个 else if,我们直接忽略掉,重点是下面这个 else if,可以看到,直接去用CAS加了,成功的话直接结束
既然说到CAS加成功,那就会有失败,失败怎么办呢?我们看这一段
看这个逻辑条件,也是尝试获取锁,获取到锁之后,那就开始用移位操作扩容,并且进行赋值,扩容为两倍(至于为啥是两倍,因为左移了1....),然后最后释放掉cellsBusy的状态
OK,到这儿为止,cells创建完成+内部为空不为空的情况已经完事了,让我们开始看cells没有创建完成的情况
同样的操作,我们收起大if
可以清楚看到,上来也是加锁,然后创建一个长度为2的数组,然后通过计算,把当前值传入到所对应的位置,然后释放锁,结束
好,如果,假设,就是拿不到锁怎么办?也就是说,cells没有创建,也还拿不到锁去创建,我们可以看到有最后一段逻辑,最后一段else
它尝试从base直接去改,改成功了,那就结束,失败了,走最外层自旋
也就是说,只能有一个线程对cells进行初始化
给大家手画一副流程逻辑图,写字不好,请将就看
到这里,是不是完全明白了这个方法的处理逻辑,以及DoubleAdder的操作逻辑?
看完了 add 方法,我们再看一个 sum 方法
public double sum() {
Cell[] as = cells; Cell a;
double sum = Double.longBitsToDouble(base);
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += Double.longBitsToDouble(a.value);
}
}
return sum;
}
可以看到,调用 sum 方法的时候,它会返回当前时刻的累加值,注意,当前时刻,也就是说,它不一定准,比如别的线程还在自己加的时候,调用了 sum,也可能,调用 sum 的时候,刚好在扩容
所以说,这个方法在高并发情况下,不是线程安全的
以上就是这次的总结整理,大家可以按照自己的实际情况来使用