其实在上篇文章阐述synchronized关键字原理时,我们曾多次谈到过CAS这个概念,那么它究竟是什么?
实际在之前为了解决多线程并行执行带来的线程安全问题,都会利用加锁的机制去将多线程并行执行,改变为单线程的串行执行。不过还有另一种手段能够去避免此类问题的发生,而这种方案和之前所分析的synchronized互斥锁的原理大相径庭,它实则是利用一种自旋的无锁形式去解决线程安全问题。
在之前分析中我们不难发现,当一个线程想要执行被synchronized修饰的代码/方法,为了避免操作共享资源时发生冲突,每次都需要先拿到锁,而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,而万事难遂人愿,一旦发现冲突,无锁策略中难道真的不处理吗?
并不然,无锁策略采用一种称为CAS的技术,来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键。
我们口中一直所提到的无锁策略,听起来很完美,但是当真正需要使用时,又该如果落地实现呢?CAS机制就是无锁策略的落地实现者,CAS全称Compare And Swap(比较并交换),Java中的CAS实现,最终也是依赖于CPU的原子性指令实现(我们稍后会分析),在CAS机制中其核心思想如下:
CAS(V,E,N)
• V:需要操作的共享变量
• E:预期值
• N:新值
如果V值等于E值,则将N值赋值给V。反之,如果V值不等于E值,则说明在当前线程写回前,有其他线程对V做了更改,那么当前线程什么都不做。
简单的来说,就是当一个线程需要对V做更改时,先在操作前,保存当前时刻共享变量的值,当线程操作完成后,需要写回新值时,先重新去获取一下最新的变量值,接着与操作开始前的保存的预期值比对,如果相同说明没有其他线程改过,那么当前线程就执行写入操作。
可是如果期望值与当前线程操作前保存的值不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量的值,尝试再次修改该变量,也可以放弃操作,示意图如下:
由于CAS操作属于乐观派,每次线程操作时,都认为自己可以成功执行,当多个线程同时使用CAS操作一个变量时,只有一个会成功执行并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
基于这样的原理,CAS机制即使没有锁,同样也能够得知其他线程对共享资源进行了变更,并执行相应的处理措施。同时CAS操作由于过程中并没有锁的存在,因此不可能出现死锁的情况,所以也能得出一个结论:“CAS天生免疫死锁”,因为CAS本身没有加锁。
那么有小伙伴看到这里会疑问,难道多个线程在同时做CAS操作时,不会出现安全问题造成不一致吗?
通过上面的阐述,我们得知CAS操作并不是一步到位的,而是分为多个步骤来执行,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值?答案非常确定:不会,Why?
刚刚在前面我提到过:Java中的CAS机制的最终实现,是依赖于CPU原子性指令实现,CAS是一种操作系统原语范畴的指令,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断。也就是说:CAS操作对于CPU来说,是一条的原子指令,不会造成所谓的数据不一致问题。
Unsafe类位于sun.misc包中,中文翻译过来就是不安全的意思,当我们第一次看到这个类时,我们可能会感到惊讶:为什么在JDK中会有一个类的名称,被命名为“不安全”?但是你详细去研究不难发现这个类的神奇之处,提供的功能十分强大,但是确实存在些许不安全。
Unsafe类,其内部方法可以像C的指针一样,直接操作内存,当我们通过这个类做到和C一样直接操作内存时,也就凸显出此类的不安全性,毕竟这意味着:
• 不受JVM管理,也就代表着无法被GC,需要我们手动释放内存,当你使用这个类做了一些操作稍有不慎就会出现内存泄漏;
• Unsafe类中的不少方法,必须提供原始内存地址,以及要被替换的对象地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的错误,会导致整个Java程序崩溃,表现为应用进程直接宕掉。
但是通过Unsafe类直接操作内存,也意味着其速度会比普通Java程序更快,在高并发的条件之下,能够很好地提高效率。因此,从上面几个角度来看,虽然提升了效率,但是也带来了一定的不安全性,Unsafe名副其实。
所以我们在编写程序时,如果没有什么特殊要求,就不应该考虑使用它,并且Java官方也不推荐使用,甚至官方都没有对外提供Unsafe的构造函数,而且Oracle官方当初也打算在Java9中去掉Unsafe类。
不过由于Java并发包中大量使用了该类,所以Oracle最终在Java9并没有移除掉Unsafe类,只是做了一定的优化与维护,而且除开并发包之外,类似于Netty等框架也在底层中频繁的使用了该类,所以应该庆幸的是保留了该类,不然会少去一批优秀的开源框架。
不过还有一点值得注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法,都直接调用操作系统底层资源执行相应任务,关于Unsafe类的主要功能点如下:
• 类(Class)相关:提供Class和它的静态域操纵方法;
• 信息(Info)相关:返回某些低级别的内存信息;
• 数组(Arrays)相关:提供数组操纵方法;
• 对象(Objects)相关:提供Object和它的域操纵方法;
• 内存(Memory)相关:提供直接内存访问方法(绕过JVM堆直接操纵本地内存);
• 同步(Synchronization)相关:提供低级别同步原语、线程挂起/放下等操纵方法。
Unsafe类提供了直接操纵内存相关的方法:
//分配内存指定大小的内存
public native long allocateMemory(long bytes);
//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);
//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);
//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//设置给定内存地址的值
public native void putAddress(long address, long x);
//获取指定内存地址的值
public native long getAddress(long address);
//设置给定内存地址的long值
public native void putLong(long address, long x);
//获取指定内存地址的long值
public native long getLong(long address);
//设置或获取指定内存的byte值
public native byte getByte(long address);
public native void putByte(long address, byte x);
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
.......... 省略代码
//操作系统的内存页大小
public native int pageSize();
在之前我们创建类对象实例,无非通过两种形式:new以及反射机制创建。
但无论是new,还是反射的形式创建,都会调用对象的构造方法来完成对象的初始化,而Unsafe类提供新的创建对象实例途径,如下:
//传入一个对象的class并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class cls) throws InstantiationException;
Unsafe类提供了类、实例对象以及变量操纵方法:
//获取字段f在实例对象中的偏移量
public native long objectFieldOffset(Field f);
//静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);
//返回值就是f.getDeclaringClass()
public native Object staticFieldBase(Field f);
//获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量的内存地址,
//通过偏移量便可得到该对象的变量,进行各种操作
public native int getInt(Object o, long offset);
//设置给定对象上偏移量的int值
public native void putInt(Object o, long offset, int x);
//获得给定对象偏移量上的引用类型的值
public native Object getObject(Object o, long offset);
//设置给定对象偏移量上的引用类型的值
public native void putObject(Object o, long offset, Object x);
//其他基本数据类型(long,char,byte,float,double)的操作与getInthe及putInt相同
//设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
public native void putIntVolatile(Object o, long offset, int x);
//获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
public native int getIntVolatile(Object o, long offset);
//其他基本数据类型(long,char,byte,float,double)的操作与putIntVolatile
//及getIntVolatile相同,引用类型putObjectVolatile也一样。
..........省略代码
//与putIntVolatile一样,但要求被操作字段必须有volatile修饰
public native void putOrderedInt(Object o,long offset,int x);
下面通过一个小Demo,来加深大家对与Unsafe类的熟悉程度。
Unsafe类没有对外提供构造函数,虽然Unsafe类对外提供getUnsafe()方法,但该方法只提供给Bootstrap类加载器使用,普通用户调用将抛出异常。
public static Unsafe getUnsafe() {
Class cc = sun.reflect.Reflection.getCallerClass(2);
if (cc.getClassLoader() != null)
throw new SecurityException("Unsafe");
return theUnsafe;
}
为此,在下面的Demo中,我们将使用反射技术,获取Unsafe实例对象并进行相关操作。
public class UnSafeDemo {
public static void main(String[] args) throws NoSuchFieldException,
IllegalAccessException, InstantiationException {
// 通过反射得到theUnsafe对应的Field对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 设置该Field为可访问
field.setAccessible(true);
// 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println(unsafe);
//通过allocateInstance直接创建对象
Demo demo = (Demo) unsafe.allocateInstance(Demo.class);
Class demoClass = demo.getClass();
Field str = demoClass.getDeclaredField("str");
Field i = demoClass.getDeclaredField("i");
Field staticStr = demoClass.getDeclaredField("staticStr");
//获取实例变量str和i在对象内存中的偏移量并设置值
unsafe.putInt(demo,unsafe.objectFieldOffset(i),1);
unsafe.putObject(demo,unsafe.objectFieldOffset(str),"Hello Word!");
// 返回 User.class
Object staticField = unsafe.staticFieldBase(staticStr);
System.out.println("staticField:" + staticStr);
//获取静态变量staticStr的偏移量staticOffset
long staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("staticStr"));
//获取静态变量的值
System.out.println("设置前的Static字段值:"+unsafe.getObject(staticField,staticOffset));
//设置值
unsafe.putObject(staticField,staticOffset,"Hello Java!");
//再次获取静态变量的值
System.out.println("设置后的Static字段值:"+unsafe.getObject(staticField,staticOffset));
//调用toString方法
System.out.println("输出结果:"+demo.toString());
long data = 1000;
byte size = 1; //单位字节
//调用allocateMemory分配内存,并获取内存地址memoryAddress
long memoryAddress = unsafe.allocateMemory(size);
//直接往内存写入数据
unsafe.putAddress(memoryAddress, data);
//获取指定内存地址的数据
long addrData = unsafe.getAddress(memoryAddress);
System.out.println("addrData:"+addrData);
/**
* 输出结果:
sun.misc.Unsafe@0f18aef2
staticField:class com.demo.Demo
设置前的Static字段值:Demo_Static
设置后的Static字段值:Hello Java!
输出USER:Demo{str='Hello Word!', i='1', staticStr='Hello Java!'}
addrData:1000
*/
}
}
class Demo{
public Demo(){
System.out.println("我是Demo类的构造函数,我被人调用创建对象实例啦....");
}
private String str;
private int i;
private static String staticStr = "Demo_Static";
@Override
public String toString() {
return "Demo{" +
"str = '" + str + '\'' +
", i = '" + i +'\'' +
", staticStr = " + staticStr +'\'' +
'}';
}
}
Unsafe类还提供了直接获取数组元素内存位置的途径:
// 获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);
// 数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,
// 可定位数组中每个元素在内存中的位置
public native int arrayIndexScale(Class arrayClass);
Unsafe类为Java语言提供了CAS操作的支持:
//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
//1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//获取内存中最新值
v = getIntVolatile(o, offset);
//通过CAS操作
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
//1.8新增,方法作用同上,只不过这里操作的long类型数据
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
//1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
//这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, newValue));
return v;
}
// 1.8新增,同上,操作的是long类型
public final long getAndSetLong(Object o, long offset, long newValue) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, newValue));
return v;
}
//1.8新增,同上,操作的是引用类型数据
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset);
} while (!compareAndSwapObject(o, offset, v, newValue));
return v;
}
如上方法如果有伙伴看过JDK8的Atomic包的源码,应该也能从中见到它们的身影。
Unsafe类提供了线程挂起与恢复操作支持。
将一个线程进行挂起是通过park方法实现的,调用park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。
Java对线程的挂起操作被封装在LockSupport类中,LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法来实现。
//线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。
public native void park(boolean isAbsolute, long time);
//终止挂起的线程,恢复正常JUC包中的挂起操作,
// 都是在LockSupport类实现的,其底层正是使用这两个方法,
public native void unpark(Object thread);
在之前《JMM、指令重排序》这篇文章中,提到过内存屏障,而Unsafe类也提供了内存屏障定义支持:
//在该方法之前的所有读操作,一定在load屏障之前执行完成
public native void loadFence();
//在该方法之前的所有写操作,一定在store屏障之前执行完成
public native void storeFence();
// 在该方法之前的所有读写操作,一定在full屏障之前执行完成,
// 这个内存屏障相当于上面两个的合体功能
public native void fullFence();
//获取持有锁,已不建议使用
@Deprecated
public native void monitorEnter(Object var1);
//释放锁,已不建议使用
@Deprecated
public native void monitorExit(Object var1);
//尝试获取锁,已不建议使用
@Deprecated
public native boolean tryMonitorEnter(Object var1);
//获取本机内存的页数,这个值永远都是2的幂次方
public native int pageSize();
//告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类
public native Class defineClass(String name, byte[] b, int off,
int len, ClassLoader loader, ProtectionDomain protectionDomain);
//加载一个匿名类
public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);
//判断是否需要加载一个类
public native boolean shouldBeInitialized(Class<?> c);
//确保类一定被加载
public native void ensureClassInitialized(Class<?> c);
更多操作请参考:JDK8官方API文档。
通过前面部分的分析,大家应该大致理解了CAS无锁思想,以及对魔法类Unsafe有较全面的认知,而这些也是我们分析atomic包的基本条件。接下来我们一步步分析CAS在Java中的应用。
JDK5之后推出的JUC并发包中提供了java.util.concurrent.atomic原子包,在该包下提供了大量基于CAS实现的原子操作类,如以后不想对程序代码加锁,但仍然想避免线程安全问题,那么便可以使用该包下提供的类,原子包提供的操作类主要可分为如下四种类型:
• 基本类型原子操作类;
• 引用类型原子操作类;
• 数组类型原子操作类;
• 属性更新原子操作类。
Atomic包提供的基本类型原子操作类分别为:AtomicInteger、AtomicBoolean、AtomicLong三个,它们的底层实现方式及使用方式是一致的,所以我们只分析其中一个AtomicInteger用来作举例。
AtomicInteger主要是针对int类型封装的原子操作类,它提供了原子自增方法、原子自减方法以及原子赋值方法等API:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 获取指针类Unsafe类实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//变量value在AtomicInteger实例对象内的内存偏移量
private static final long valueOffset;
static {
try {
//通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移
//通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//当前AtomicInteger封装的int变量值 value
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
//获取当前最新值,
public final int get() {
return value;
}
//设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全
public final void set(int newValue) {
value = newValue;
}
//最终会设置成newValue,使用该方法后可能导致其他线程在之后的
// 一小段时间内可以获取到旧值,有点类似于延迟加载
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
//设置新值并获取旧值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//如果当前值为expect,则设置为update(当前值指的是value变量)
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//当前值自增加1,返回旧值,底层CAS操作
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//当前值自减扣1,返回旧值,底层CAS操作
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
//当前值增加delta,返回旧值,底层CAS操作
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//当前值自增加1并返回自增后的新值,底层CAS操作
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//当前值自减扣1并返回自减之后的新值,底层CAS操作
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
//当前值增加delta,返回新值,底层CAS操作
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
//省略一些不常用的方法....
}
从上面的源码中我们可以得知,AtomicInteger原子类的所有方法中,并没有使用到任何互斥锁机制来实现同步,而是通过我们前面介绍的Unsafe类,其中提供的CAS操作来保障的线程安全,在我们前面关于《Synchronized原理分析》的文章中讲到过:i++其实存在线程安全问题,那么我们在来观察AtomicInteger原子类中的自增实现incrementAndGet()方法:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
从如上源码我们可以看到,最终的实现是调用了unsafe.getAndAddInt()方法来完成的,这个方法在我们前面的分析中得知:这是JDK8之后基于原有cas操作新增的方法,我们进一步跟进:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
可看出getAndAddInt()通过一个do while死循环不断的重试更新要设置的值,直到成功为止。内部调用的是Unsafe类中的compareAndSwapInt()方法,这是一个CAS操作方法。
注意:
Java8之前的源码实现中,是for循环,而且是直接在AtomicInteger类中实现的自增逻辑,在Java8中,被换成了while以及被挪动到Unsafe类中实现。
下面来通过一个简单的小demo掌握一下基本类型的原子操作类用法:
public class AtomicIntegerDemo {
// 创建共享变量 atomicI
static AtomicInteger atomicI = new AtomicInteger();
public static class AddThread implements Runnable{
public void run(){
for(int i = 0; i < 10000; i++)
atomicI.incrementAndGet();
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
//开启5条线程同时执行atomicI的自增操作
for(int i = 1; i <= 5; i++){
threads[i]=new Thread(new AddThread());
}
//启动线程
for(int i = 1; i <= 5; i++){threads[i].start();}
for(int i = 1; i <= 5; i++){threads[i].join();}
System.out.println(atomicI);
}
}
//输出结果:50000
在上述demo中使用AtomicInteger代替原本的int,这样就能做到不加锁也能保证线程安全,而AtomicBoolean以及AtomicLong将不再分析,使用以及原理都是一样的。
引用类型的原子操作类主要分析AtomicReference类,其他的原理及使用都是一致的,后续内容不再重复阐述这段话,先来看一个Demo:
public class AtomicReferenceDemo {
public static AtomicReference<Student> atomicStudentRef = new AtomicReference<Student>();
public static void main(String[] args) {
Student s1 = new Student(1, "竹子");
atomicStudentRef.set(s1);
Student s2 = new Student(2, "熊猫");
atomicStudentRef.compareAndSet(s1, s2);
System.out.println(atomicStudentRef.get().toString());
//执行结果:Student{id=2, name="熊猫"}
}
static class Student {
private int id;
public String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Student{" +"id=" + id +", name=" + name +"}";
}
}
}
在此之前我们分析了原子计数器AtomicInteger类,它的底层实现是通过Unsafe类中CAS操作来实现的,那么我们现在的AtomicReference底层又是怎么实现的呢?
public class AtomicReference<V> implements java.io.Serializable {
// 得到unsafe对象实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 定义值偏移量
private static final long valueOffset;
static {
try {
/* 静态代码块在类加载时为偏移量赋值,通过unsafe类提供的得到类属性的
地址API得到当前类定义的属性value的地址 */
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//内部变量value,Unsafe类通过valueOffset内存偏移量即可获取该变量
private volatile V value;
/* 原子替换方法,间接调用Unsafe类的compareAndSwapObject(),
它是一个实现了CAS操作的本地(native)方法 */
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
//设置并获取旧值
public final V getAndSet(V newValue) {
return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
}
//省略代码......
}
//Unsafe类中的getAndSetObject方法,实际调用还是CAS操作
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset);
} while (!compareAndSwapObject(o, offset, v, newValue));
return v;
}
从上述源码中我们可以得知,AtomicReference与我们前面分析的AtomicInteger实现的原理大致相同,最终都是通过Unsafe类中提供的CAS操作来实现的,关于AtomicReference的其他方法实现原理也大致相同,只不过需要注意的是Java8为AtomicReference新增了几个API:
• getAndUpdate(UnaryOperator<V>)
• updateAndGet(UnaryOperator<V>)
• getAndAccumulate(V,AnaryOperator<V>)
• accumulateAndGet(V,AnaryOperator<V>)
上述的方法几乎存在于所有的原子类中,而这些方法可以基于Lambda表达式,对传递进来的期望值或要更新的值,进行其他操作后再进行CAS操作,说人话就是:对期望值或要更新的值,可以进行额外修改后再执行CAS更新。
大家刚开始接触Java时,学习的数组,操作其中元素时都会存在线程安全问题,而我们数组类型原子操作类的含义其实非常简单,指的就是利用原子的形式,更新数组中的元素,从而避免出现线程安全问题。而JUC包中的数组类型原子操作类,具体分为以下三个类:
• AtomicIntegerArray:原子更新整数数组里的元素;
• AtomicLongArray:原子更新长整数数组里的元素;
• AtomicReferenceArray:原子更新引用类型数组里的元素。
同样也只分析单个类,其他两个大致相同,此处以AtomicIntegerArray举例,先上一个Demo案例:
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray atomicArr = new AtomicIntegerArray(5);
public static class incrementTask implements Runnable{
public void run(){
// 执行数组中元素自增操作,参数为index,即数组下标
for(int i = 0; i < 10000; i++)
atomicArr.getAndIncrement(i % atomicArr.length());
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[5];
for(int i = 0; i < 5;i++)
threads[i] = new Thread(new incrementTask());
for(int i = 0; i < 5;i++) threads[i].start();
for(int i = 0; i < 5;i++) threads[i].join();
System.out.println(atomicArr);
/* 执行结果:
[10000, 10000, 10000, 10000, 10000]
*/
}
}
如上Demo中,我们启动了五条线程对数组中的元素进行自增操作,执行的结果符合我们的预期。
接下来再一步步的去分析AtomicIntegerArray内部的实现逻辑,源码如下:
public class AtomicIntegerArray implements java.io.Serializable {
//获取Unsafe实例对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
//从前面我们分析Unsafe类得知arrayBaseOffset()作用:获取数组的第一个元素内存起始地址
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int shift;
//内部数组
private final int[] array;
static {
//获取数组中一个元素占据的内存空间
int scale = unsafe.arrayIndexScale(int[].class);
//判断是否为2的次幂,一般为2的次幂否则抛异常
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
//
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
private long checkedByteOffset(int i) {
if (i < 0 || i >= array.length)
throw new IndexOutOfBoundsException("index " + i);
return byteOffset(i);
}
//计算数组中每个元素的的内存地址
private static long byteOffset(int i) {
return ((long) i << shift) + base;
}
//省略代码......
}
在我们前面的Unsafe分析中得知:arrayBaseOffset可以获取一个数组中、第一个元素内存起始位置,而Unsafe还有另一个API:arrayIndexScale(),则可以得知数组中指定下标元素的内存占用大小。
而目前的AtomicIntegerArray是int数组的包装类型,int的大小是占用4Byte(字节),所以scale的值为4。现在如何根据这两个信息,计算数组中每个元素的内存起始地址呢?稍作推导方能得出如下结论:
数组每个元素起始内存位置 = 数组第一个元素起始位置 + 数组元素下标 * 数组中每个元素占用的内存空间大小
而上述源码中的byteOffset(i)方法,则是这个公式的体现。
有人可能会看到这里有些疑惑,该方法的实现似乎与我刚刚描述的结论有出入。别急,我们先来看看shift这个值是怎么得来的:
shift = 31 - Integer.numberOfLeadingZeros(scale);
Integer.numberOfLeadingZeros(scale)这行代码的作用是:计算出scale的前导零个数(转换为二进制后前面连续的0数量,称为前零导数),scale=4,转成二进制为00000000 00000000 00000000 00000100,所以前零导数为29,所以shift值则为31-29=2。
回到最开始的问题:数组中每个元素的起始内存位置怎么计算呢?
利用我们刚刚得到的shift值套入byteOffset(i)方法中,计算数组中每个元素的起始内存位置(数组下标不越界情况下):
byteOffset方法体:(i << shift) + base(i为index,即数组元素下标;base为数组第一个元素起始内存位置)。
• 第一个元素:memoryAddress = 0 << 2 + base 即 memoryAddress = base + 0 * 4
• 第二个元素:memoryAddress = 1 << 2 + base 即 memoryAddress = base + 1 * 4
• 第三个元素:memoryAddress = 2 << 2 + base 即 memoryAddress = base + 2 * 4
• 第四个元素:memoryAddress = 3 << 2 + base 即 memoryAddress = base + 3 * 4
• 其他元素位置以此类推.......
上述则是AtomicIntegerArray.byteOffset方法的原理,所以byteOffset(int)方法可以根据数组下标计算出每个元素的内存地址。而AtomicIntegerArray中的其他方法都是间接调用Unsafe类的CAS原子操作方法实现,下面简单看其中几个常用方法:
//执行自增操作,返回旧值,入参i是index即数组元素下标
public final int getAndIncrement(int i) {
return getAndAdd(i, 1);
}
//指定下标元素执行自增操作,并返回新值
public final int incrementAndGet(int i) {
return getAndAdd(i, 1) + 1;
}
//指定下标元素执行自减操作,并返回新值
public final int decrementAndGet(int i) {
return getAndAdd(i, -1) - 1;
}
//间接调用unsafe.getAndAddInt()方法
public final int getAndAdd(int i, int delta) {
return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}
//Unsafe类中的getAndAddInt方法,执行CAS操作
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
AtomicReferenceArray与AtomicLongArray的原理也大致类似,为此不再阐述。
如果我们只需要一个类的某个字段(类属性)也变为原子操作,即让某个普通变量也变为原子操作,可以使用属性更新原子操作类。如在某些时候项目需求发生变化,使得某个类中,之前不需要涉及多线程操作的某个属性,变为需要参与多线程的执行,由于在编码时该属性被多处使用,改动起来代码侵入性比较高,并且比较复杂。
而且之前的代码中,使用该属性的地方无需考虑线程安全,只要求新场景需要保证线程安全时,就可以借助原子更新器处理这种场景,J.U.C.Atomic并发原子包提供了如下三个类:
• AtomicIntegerFieldUpdater:更新整型的属性的原子操作类
• AtomicLongFieldUpdater:更新长整型属性的原子操作类
• AtomicReferenceFieldUpdater:更新引用类型中属性的原子操作类
不过值得令人注意的是:使用原子更新类的条件是比较苛刻的,如下:
• 操作的字段不能被static修饰;
• 操作的字段不能被final修饰,因为常量无法修改;
• 操作的字段必须是被volatile修饰保证可见性,也就是需要保证数据的读取是线程可见的;
• 属性必须对当前Updater所在的区域是可见的,如果不是当前类内部进行原子更新器操作,不能使用private、protected修饰符。子类操作父类时,修饰符必须是protected权限及以上;如果在同一个package下,则必须是default权限及以上;也就是说无论何时,都应该保证操作类与被操作类间的可见性。
来个简单的小Demo感受一下:
public class AtomicIntegerFieldUpdaterDemo{
// 定义更新整型的属性的原子操作类,目标属性:Course.courseScore
static AtomicIntegerFieldUpdater<Course> courseAIFU =
AtomicIntegerFieldUpdater.newUpdater(Course.class, "courseScore");
// 定义更新引用类型中属性的原子操作类,目标属性:Student.studentName
static AtomicReferenceFieldUpdater<Student,String> studentARFU =
AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"studentName");
// 定义原子计数器效验数据准确性
public static AtomicInteger courseScoreCount = new AtomicInteger(0);
public static void main(String[] args) throws Exception{
final Course course = new Course();
Thread[] threads = new Thread[1000];
for(int i = 0;i < 1000;i++){
threads[i] = new Thread(()->{
if(Math.random()>0.6){
courseAIFU.incrementAndGet(course);
courseScoreCount.incrementAndGet();
}
});
threads[i].start();
}
for(int i = 0;i < 1000;i++) threads[i].join();
System.out.println("Course.courseScore:" + course.courseScore);
System.out.println("数据效验结果:" + courseScoreCount.get());
// 更新引用类型中属性的原子操作类的demo
Student student = new Student(1,"竹子");
studentARFU.compareAndSet(student,student.studentName,"熊猫");
System.out.println(student.toString());
}
public static class Course{
int courseId;
String courseName;
volatile int courseScore;
}
public static class Student{
int studentId;
volatile String studentName;
public Student(int studentId,String studentName){
this.studentId = studentId;
this.studentName = studentName;
}
@Override
public String toString() {
return "Student[studentId="+studentId+"studentName="+studentName+"]";
}
}
}
/** 运行结果:
* Course.courseScore:415
* 数据效验结果:415
* Student[studentId=1studentName=熊猫]
**/
我们从上面的代码中,存在Course、Student这两个内部静态类,我们使用AtomicIntegerFieldUpdater作用在Course.courseScore属性上,使用AtomicReferenceFieldUpdater作用在Student.studentName上。
在上面的代码中,我们开启一千条线程进行逻辑操作,使用AtomicInteger定义计数器courseScoreCount进行数据效验,其目的在于效验AtomicIntegerFieldUpdater是否能保证线程安全问题。
当我们开启的线程,随机出的数字大于0.6时,代表成绩合格,此时将这次的成绩录入进Course.courseScore中,然后再对courseScoreCount进行自增,方便后续进行数据效验。最终结果输出Course.courseScore与courseScoreCount结果一致,也就代表着AtomicIntegerFieldUpdater能够正确的更新Course.courseScore值,且能够保证线程安全问题。
而对于AtomicReferenceFieldUpdater,在上面的代码中,简单的编写了demo演示了其使用方式,我们在使用AtomicReferenceFieldUpdater的时候,值得注意的一点就是需要传入两个泛型参数,一个是修改的类的class对象,另一个是修改的Field的class对象。
至于另外的AtomicLongFieldUpdater则不再演示,使用方式与AtomicIntegerFieldUpdater大致相同。
至此,我们再研究一下AtomicIntegerFieldUpdater的实现原理,先上代码:
public abstract class AtomicIntegerFieldUpdater<T> {
public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,
String fieldName){
return new AtomicIntegerFieldUpdaterImpl<U>
(tclass, fieldName, Reflection.getCallerClass());
}
}
从源码中不难发现,其实AtomicIntegerFieldUpdater的定义是一个抽象类,最终实现类为AtomicIntegerFieldUpdaterImpl,进一步跟进AtomicIntegerFieldUpdaterImpl:
private static class AtomicIntegerFieldUpdaterImpl<T>
extends AtomicIntegerFieldUpdater<T> {
// 获取unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 定义内存偏移量
private final long offset;
private final Class<T> tclass;
private final Class<?> cclass;
// 构造方法
AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
final String fieldName,
final Class<?> caller) {
final Field field;// 要修改的字段
final int modifiers;// 字段修饰符
try {
field = AccessController.doPrivileged(
new PrivilegedExceptionAction<Field>() {
public Field run() throws NoSuchFieldException {
// 通过反射获取字段对象
return tclass.getDeclaredField(fieldName);
}
});
// 获取字段修饰符
modifiers = field.getModifiers();
//对字段的访问权限进行检查,不在访问范围内抛异常
sun.reflect.misc.ReflectUtil.ensureMemberAccess(
caller, tclass, null, modifiers);
// 得到对应类对象的类加载器
ClassLoader cl = tclass.getClassLoader();
ClassLoader ccl = caller.getClassLoader();
if ((ccl != null) && (ccl != cl) &&
((cl == null) || !isAncestor(cl, ccl))) {
sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
}
} catch (PrivilegedActionException pae) {
throw new RuntimeException(pae.getException());
} catch (Exception ex) {
throw new RuntimeException(ex);
}
Class<?> fieldt = field.getType();
// 判断是否为int类型
if (fieldt != int.class)
throw new IllegalArgumentException("Must be integer type");
// 判断是否被volatile修饰
if (!Modifier.isVolatile(modifiers))
throw new IllegalArgumentException("Must be volatile type");
this.cclass = (Modifier.isProtected(modifiers) &&
caller != tclass) ? caller : null;
this.tclass = tclass;
// 获取该字段的在对象内存的偏移量,通过内存偏移量可以获取或者修改该字段的值
offset = unsafe.objectFieldOffset(field);
}
}
从AtomicIntegerFieldUpdaterImpl源码中不难看出:其实字段更新器都是通过反射机制+Unsafe类实现的,看看AtomicIntegerFieldUpdaterImpl中,自增方法incrementAndGet的实现:
public int incrementAndGet(T obj) {
int prev, next;
do {
prev = get(obj);
next = prev + 1;
// 调用下面的compareAndSet方法完成cas操作
} while (!compareAndSet(obj, prev, next));
return next;
}
// 最终调用的还是Unsafe类中compareAndSwapInt()方法完成
public boolean compareAndSet(T obj, int expect, int update) {
if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
return unsafe.compareAndSwapInt(obj, offset, expect, update);
}
我们到这可以发现,其实关于J.U.C.Atomic并发原子包,其中所有原子类的最终实现,都是通过前面所分析的Unsafe类实现的,包括之后会谈到的很多并发知识,如AQS等,都会牵扯到CAS机制,所以记住一点:**CAS是整个JUC并发包的基石!**
当第一个线程执行CAS(V,E,N)操作,在获取到当前变量V,准备修改为新值N前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为第一个线程看到的原有值,这样的话,我们就无法正确判断这个变量是否已被修改过!
简单举例:线程T1、T2、T3,共享变量atomicI=0,当T1准备将atomicI更改为1时,此时T1线程看到的值是0,并将atomicI=0读取回工作内存,然而在T1线程进行操作的过程中,T2线程将值更新为了1,然后T3线程又将atomicI更改成了0。
这种情况下,当T1再回来进行更新时,就无法感知这个值已经被其他线程变更过,在做V==E判断时,发现atomicI还是原先的值(0),此时就会对atomicI进行更改操作,但其实这时的“现场”,与T1线程第一次看到的“现场”已经不同了。如下图:
这就是CAS机制存在的ABA问题,不过发生的概念很小,而且就算发生了,一般也不会对业务造成影响。比如上面的例子,就算出现了ABA问题,也不会影响数据的最终一致性,T1线程的预期值本身就是1,出现了ABA问题,T1线程更新后的值还是1。
但是在某些情况下,还是需要避免此类问题的,比如一个单向链表实现的堆栈stack如下:
此时栈顶元素为A,T1线程已经知道A.next为元素B,希望通过CAS机制将栈顶元素替换为B,执行如下:
stackHead.compareAndSet(A,B);
在T1线程执行这行代码时,T2线程介入,线程T2将A、B出栈,然后再将元素Y、X、A入栈,此时元素Y、X、A处于栈内,元素B处于孤立状态,示意图如下:
正好这时T1执行CAS写回操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:
现在堆栈中只有B元素,Y、X元素不位于堆栈中,但T2线程的确执行了Y、X入栈操作,就这样平白无故的将X、Y丢掉了。
总而言之,只要一个场景,存在需要基于动态变化而要做出操作时,出现ABA问题就需要解决;但是如果你的应用,只停留在基于表面数据进行判断,那么ABA问题就不用去关注。
当解决出现的ABA问题时,又该怎么办呢?比如在通过version版本号实现乐观锁,从而避免并发带来的问题时,假设产生了ABA问题,这时该怎么处理?在Java中解决ABA问题主要有两种方案。
• AtomicStampedReference:时间戳控制,能够完全解决;
• AtomicMarkableReference:维护boolean值控制,不能完全杜绝。
AtomicStampedReference是一个带有时间戳的对象引用,内部通过包装Pair对象键值对的形式,来存储数据与时间戳,在每次更新时,先对数据本身和时间戳进行比对,只有当两者都符合预期值时,才调用Unsafe的compareAndSwapObject方法进行写入。
当然,AtomicStampedReference不仅会设置新值,而且还会记录更改的时间戳。这也就解决了之前CAS机制带来的ABA问题。简单案例如下:
public class ABAIssue {
// 定义原子计数器,初始值 = 100
private static AtomicInteger atomicI = new AtomicInteger(100);
// 定义AtomicStampedReference:初始化时需要传入一个初始值和初始时间
private static AtomicStampedReference<Integer> asRef = new AtomicStampedReference<Integer>(100, 0);
/**
* 未使用AtomicStampedReference线程组:TA TB
*/
private static Thread TA = new Thread(() -> {
System.err.println("未使用AtomicStampedReference线程组:[TA TB] >>>>");
// 更新值为101
boolean flag = atomicI.compareAndSet(100, 101);
System.out.println("线程TA:100 -> 101.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
// 更新值为100
flag = atomicI.compareAndSet(101, 100);
System.out.println("线程TA:101 -> 100.... flag:" + flag + ",atomicINewValue:" + atomicI.get());
});
private static Thread TB = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = atomicI.compareAndSet(100, 888);
System.out.println("线程TB:100 -> 888.... flag:" + flag + ",atomicINewValue:" + atomicI.get() + "\n\n");
});
/**
* 使用AtomicStampedReference线程组:T1 T2
*/
private static Thread T1 = new Thread(() -> {
System.err.println("使用AtomicStampedReference线程组:[T1 T2] >>>>");
// 更新值为101
boolean flag = asRef.compareAndSet(100, 101, asRef.getStamp(), asRef.getStamp() + 1);
System.out.println("线程T1:100 -> 101.... flag:" + flag + ".... asRefNewValue:"
+ asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
// 更新值为100
flag = asRef.compareAndSet(101, 100, asRef.getStamp(), asRef.getStamp() + 1);
System.out.println("线程T1:101 -> 100.... flag:" + flag + ".... asRefNewValue:"
+ asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
});
private static Thread T2 = new Thread(() -> {
int time = asRef.getStamp();
System.out.println("线程休眠前Time值:" + time);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = asRef.compareAndSet(100, 888, time, time + 1);
System.out.println("线程T2:100 -> 888.... flag:" + flag +
".... asRefNewValue:" + asRef.getReference() + ".... 当前Time:" + asRef.getStamp());
});
public static void main(String[] args) throws InterruptedException {
TA.start();
TB.start();
TA.join();
TB.join();
T1.start();
T2.start();
}
}
/**
* 未使用AtomicStampedReference线程组:[TA TB] >>>>
* 线程TA:100 -> 101.... flag:true,atomicINewValue:101
* 线程TA:101 -> 100.... flag:true,atomicINewValue:100
* 线程TB:100 -> 888.... flag:true,atomicINewValue:888
*
*
* 使用AtomicStampedReference线程组:[T1 T2] >>>>
* 线程休眠前Time值:0
* 线程T1:100 -> 101.... flag:true.... asRefNewValue:101.... 当前Time:1
* 线程T1:101 -> 100.... flag:true.... asRefNewValue:100.... 当前Time:2
* 线程T2:100 -> 888.... flag:false.... asRefNewValue:100.... 当前Time:2
*/
观察Demo中AtomicInteger与AtomicStampedReference的测试结果,可以得知:AtomicStampedReference确实能解决之前阐述的ABA问题,那AtomicStampedReference究竟是如何解决ABA问题的呢?接下来再看看它的内部实现:
public class AtomicStampedReference<V> {
// 通过Pair内部类存储数据和时间戳
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;
// 构造方法:初始化时需传入初始值和时间初始值
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
}
compareAndSet方法源码实现:
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)));
}
// 最终实现调用Unfase类中的compareAndSwapObject()方法
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
在compareAndSet方法源码实现中,我们不难发现,它同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,而casPair()方法也是一个原子性方法,最终实现调用的还是Unsafe类中的compareAndSwapObject()方法。
AtomicMarkableReference与前面的AtomicStampedReference并不相同,AtomicMarkableReference只能在一定程度上减少ABA问题的出现,它并不能完全的杜绝ABA问题。
因为AtomicMarkableReference内部维护的是boolean类型的标识,它并不像AtomicStampedReference内部的维护的时间戳一样,值会不断的递增。AtomicMarkableReference内部因为维护的是boolean,也就代表着只会在true与false两种状态之间来回切换,所以还是存在ABA问题出现的概率,再来个demo感受一下:
public class ABAIssue {
static AtomicMarkableReference<Integer> amRef = new AtomicMarkableReference<Integer>(100, false);
private static Thread t1 = new Thread(() -> {
boolean mark = amRef.isMarked();
System.out.println("线程T1:修改前标志 Mrak:" + mark + "....");
// 将值更新为200
System.out.println("线程T1:100 --> 200.... 修改后返回值 Result:"
+ amRef.compareAndSet(amRef.getReference(), 200, mark, !mark));
});
private static Thread t2 = new Thread(() -> {
boolean mark = amRef.isMarked();
System.out.println("线程T2:修改前标志 Mrak:" + mark + "....");
// 将值更新回100
System.out.println("线程T2:200 --> 100.... 修改后返回值 Result:"
+ amRef.compareAndSet(amRef.getReference(), 100, mark, !mark));
});
private static Thread t3 = new Thread(() -> {
boolean mark = amRef.isMarked();
System.out.println("线程T3:休眠前标志 Mrak:" + mark + "....");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean flag = amRef.compareAndSet(100, 500, mark, !mark);
System.out.println("线程T3: 100 --> 500.... flag:" + flag + ",newValue:" + amRef.getReference());
});
public static void main(String[] args) throws InterruptedException {
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();
/**
* 输出结果如下:
* 线程T1:修改前标志 Mrak:false....
* 线程T1:100 --> 200.... 修改后返回值 Result:true
* 线程T2:修改前标志 Mrak:true....
* 线程T2:200 --> 100.... 修改后返回值 Result:true
* 线程T3:休眠前标志 Mrak:false....
* 线程T3: 100 --> 500.... flag:true,newValue:500
*/
/* t3线程执行完成后结果还是成功更新为500,代表t1、t2
线程所做的修改操作对于t3线程来说还是不可见的 */
}
}
从如上demo中,我们可以发现,其实使用AtomicMarkableReference后,仍然出现了ABA问题,并没有真正的避免了ABA问题的发生,那么关于AtomicMarkableReference的原理我们也不再阐述(大致和前面分析的AtomicStampedReference类似,有兴趣的小伙伴可以自行研究~)。
从前面对
ABA问题的讨论得知:AtomicMarkableReference能在一定程度上减少ABA问题出现的几率,但是无法做到完全的杜绝,而如果需要彻底避免ABA问题,还是需要使用AtomicStampedReference。
在本文开头我们曾谈到过CAS无锁的概念,其实CAS在我们上篇文章:《Synchronized关键字实现原理剖析》锁升级膨胀过程中也多次提到。
而无锁也在有些地方也被称作自旋锁,自旋是一种执行空循环的现象,比如现在出现了线程竞争,但是此处线程需要等待的时间非常短,某些没有获取到资源的线程,也能在不久后的将来,快速获取到资源并执行,OS让没有获取到资源的线程,执行几个空循环,等待资源的情况,这就是种自旋的表现。
不过自旋并不能称为真正意义上的
CAS,因为CAS是比较、替换,而自旋仅仅只是空循环。
自旋这种操作也的确可以提升效率,因为自旋锁只是在逻辑上阻塞了线程,在用户态让线程停止了代码执行,并没有在内核态中挂起对应的内核线程,从而可以减少切态、上下文切换等操作耗费的资源。
但问题是当线程越来越多,竞争十分激烈时,自旋占用CPU的时间变长,会导致性能急剧下降,因此Java虚拟机内部,一般对于自旋锁都有一定的次数限制,可能是50、100次循环后就放弃自旋,进而在OS层面挂起内核线程,让出CPU资源,停止空循环带来的资源损耗。
• 《深入理解JVM虚拟机》
• 《Java并发编程之美》
• 《Java高并发程序设计》
• 《亿级流量网站架构核心技术》
• 《Java并发编程实战》
原文作者:竹子爱熊猫;如有侵权,请联系管理员删除。