并发六:CAS与原子变量

CAS

CAS(Compare-And-Swap)是CPU的原子指令,中文翻译成比较交换,汇编指令为CMPXCHG。
CAS操作包含三个操作数内存值V、预期原值A和新值B,当且仅当预期原值A和内存值V相同时,将内存值V修改为新值B,否则,处理器不做任何操作。

// CAS C++示意
int compare_and_swap (int* reg, int oldval, int newval) {  
    int old_reg_val = *reg;  
    ATOMIC();
    if(old_reg_val == oldval)  
        *reg = newval;  
    END_ATOMIC();
    return old_reg_val;  
}

CAS:"我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可“

CAS是一种乐观加锁策略,在进行争用操作(读-修改-写)时操作线程总是认为操作会成功,如果不成功则立即返回,确保一个时刻只有一个线程写成功。相比基于MutexLock线程互斥,CAS原子操作不用挂起线程,减少了调度线程的资源开销,另外大部分CUP都支持CAS指令,硬件级的同步原语也使得CAS更加轻量级。

CAS用在了哪里?
1、JVM对内置锁synchronized的优化中引入的偏向锁和轻量都是基于CAS实现的,使用CAS操作替换对象头中的ThreadID和MarkWord来实加锁和解锁,比重量级锁效率有所提升。

2、CAS更是整个J.U.C包的基石,原子变量、同步器、并发容器等都大量的依赖CAS。
J.U.C是java.util.concurrent包的简称,鼎鼎大名的Doug Lea的作品,在J.U.C之前,java并发编程只能靠final、volatile、sychronized三个同步原语实现,J.U.C中实现了很多高效的同步工具、原子变量、并发容器等等。


JUC实现依赖

ABA问题
CAS操作时以预期原值和当前内存值是否相等作为是否修改的依据,这就会出现ABA问题。所谓ABA是指如果变量V的值原来A,变成了B,又变成了A,CAS进行检查时会发现它的值没有发生变化,但实际上变量V已经被修改过了。如果只关注最终结果,不关注中间状态是如何变化的,那么绝大部分情况下ABA问题是不会对操作结果什么影响的。

但是如果需要关注变量的变化过程,比如,一个链表A-B-C,线程1执行CAS(A,B)准备将头结点换成B,这时线程2先执行了CAS(B,C),此时链表为A-C,节点B被拿出了,CUP切换到线程1,线程1发现节点A没有变化将节点A换成节点B,因为B-next为null,节点C也被从链表上删除了,因为链表的头节点没有发生变化,所以CAS操作链表头是允许的,但是链表本身已经发生过变化。
ABA问题对程序的影响:会漏掉一些监控时间窗口,对于依赖过程值的运算会产生影响。
解决ABA问题的方法就是给变量增加版本号,每一次CAS更新操作都对版本号进行累加,变量值和版本号同时作为CAS比较的目标,只要能保证版本号一直累加就不会出现ABA问题,J.U.C中的有专门处理这个问题的类。

Unsafe

sun.misc.Unsafe是一个特殊的类,顾名思义"不安全",它包含了一些系统底层的指令:

  /**返回指定field内存地址偏移量*/
  public native long objectFieldOffset(Field field);
  /**返回指定静态field内存地址偏移量*/
  public native long staticFieldOffset(Field field);
  /**获取给定数组中第一个元素的偏移地址*/
  public native int arrayBaseOffset(Class arrayClass);
  /**CAS设置Int*/
  public native boolean compareAndSwapInt(Object obj, long offset,
                                          int expect, int update);
  /**CAS设置Long*/
  public native boolean compareAndSwapLong(Object obj, long offset,
                                          long expect, long update);
  /**CAS设置Ojbect*/
  public native boolean compareAndSwapObject(Object obj, long offset,
                                        Object expect, Object update);
  /***
   * 设置obj对象中offset偏移地址对应的整型field的值为指定值。
   * 这是一个有序或者有延迟的方法,并且不保证值的改变被其他线程立即看到。
   * 只有在field被修饰并且期望被意外修改的时候使用才有用。
   */
  public native void putOrderedInt(Object obj, long offset, int value);
  public native void putOrderedLong(Object obj, long offset, long value);
  public native void putOrderedObject(Object obj, long offset, Object value);
  /***
   * 设置obj对象中offset偏移地址对应的整型field的值为指定值。
   * 支持volatile  store语义
   */
  public native void putIntVolatile(Object obj, long offset, int value);
  public native void putLongVolatile(Object obj, long offset, long value);
  public native void putObjectVolatile(Object obj, long offset, Object value);
  /***
   * 设置obj对象中offset偏移地址对应的long型field的值为指定值。
   */
  public native void putInt(Object obj, long offset, int value);
  public native void putLong(Object obj, long offset, long value);
  public native void putObject(Object obj, long offset, Object value);
  /***
   * 获取obj对象中offset偏移地址对应的整型field的值,支持volatile load语义。
   */
  public native int getIntVolatile(Object obj, long offset);
  public native long getLongVolatile(Object obj, long offset);
  public native Object getObjectVolatile(Object obj, long offset);
  /***
   * 获取obj对象中offset偏移地址对应类型field的值
   */
  public native long getInt(Object obj, long offset);
  public native long getLong(Object obj, long offset);
  public native Object getObject(Object obj, long offset);
  /**挂起线程*/
  public native void park(boolean isAbsolute, long time);
  /**唤醒线程*/
  public native void unpark(Thread thread);
  /**内存操作*/
  public native long allocateMemory(long l);
  public native long reallocateMemory(long l, long l1);
  public native void freeMemory(long l);

compareAndSwapXXX就是CAS方法,分别对应int,long,Object三种数据类型的CAS操作,参数o是需要更新的对象、offset是对象字段在内存中的偏移量,expected是对象字段的预期原值,x是要更新的新值。
park、unpark分别是挂起和唤醒线程。
putXXXVolatile、getXXXVolatile方法是以Volatile语义设置和获取属性值。
XXXMemory方法是内存分配的操作,在java NIO和netty中会大量使用。

向来以安全著称的java肯定不会把这个类开放给用户使用,Unsafe在构造方法和工厂方法中做了限制。

private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
public static Unsafe getUnsafe() {
    Class cc = sun.reflect.Reflection.getCallerClass(2);
    if (cc.getClassLoader() != null)
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

jre/目录下的核心库是使用BootstrapClassLoader来加载的,它是虚拟机的一部分用C++编写,JAVA代码根本无获取它的引用。所以"cc"为空说明是jdk核心类库调用返回theUnsafe,不为空说明是非jdk核心类库调用就会抛出异常。
但是可以通过反射的方式来获取Unsafe实例,测试下还是可以的,但是在项目中千万别乱用。

// 反射获取Unsafe
public static Unsafe  getUnsafe() {
    Field theUnsafe = null;
    Unsafe instance = null;
    try {
        theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        instance = (Unsafe) theUnsafe.get(null);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return instance;
}

原子变量

java.util.concurrent.atomic包中,提供了包含基本类型和引用类型的12中原子变量:

基本类型

  1. AtomicBoolean 原子布尔型
  2. AtomicInteger 原子整型
  3. AtomicLong 原子长整型

引用类型

  1. AtomicReference 原子引用类型
  2. AtomicMarkableReference原子标记位引用类型
  3. AtomicStampedReference原子版本号引用类型

字段类型

  1. AtomicIntegerFieldUpdater原子整型字段更新器
  2. AtomicLongFieldUpdater原子长整型字段更新器
  3. AtomicReferenceFieldUpdater原子应用字段更新器

数组类型

  1. AtomicIntegerArray 原子整型数组
  2. AtomicLongArray 原子长整型数组
  3. AtomicReferenceArray 原子引用数组

原子变量,能够在不使用同步操作的情况下具有原子性。而Volatile关键字只能保证变量单次操作的原子性,"i++"这样的复合操作要想使其具有原子性只能加锁同步。

AtomicInteger实现分析

//int 值
private volatile int value;
public final int get() {
     return value;
}
// 省略代码 ... ... 
// 累加,并返回旧值
public final int getAndIncrement() {
    for (;;) {//循环CAS
        int current = get();// 获取当前值
        int next = current + 1;// //累加后的值 新值
        if (compareAndSet(current, next))
            return current;//CAS 更新,如果更新成功返回
    }
}
//unsafe CAS 更新
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

value变量是volatile修饰的,具有可见性,get()方法获取的当前值肯定是最新值。
将其current+1作为新值next,在循环体中进行CAS设置,直到成功才退出在此期间不用担心其他线程的干扰,因为一旦有其他线程视图修改value,就会致使compareAndSet失败并立即返回重试。

getAndIncrement和incrementAndGet是两个常用的方法,get在前说是先返回再加1即返回来的是旧值,get在后返回的是新值。

AtomicIntegerFieldUpdater使用方法

AtomicIntegerFieldUpdater用来CAS操作对象里面的int类型的字段,是一个抽象类,其具体实现由私有内部类AtomicIntegerFieldUpdaterImpl完成,AtomicIntegerFieldUpdater无法直接实例化,需使用静态工厂方法newUpdater获取其实例。

static class Person {
    volatile int age;
    public Person(int age) {
        this.age = age;
    }
}
public void testUpdate() {
    Person person = new Person(30);
    AtomicIntegerFieldUpdater<Person> updater = 
        AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");
    boolean succeed = updater.compareAndSet(person, 30, 31);
    Assert.true(succeed);
    boolean failure  = updater.compareAndSet(person, 32, 33);
    Assert.false(failure);
}

要更新的字段age必须是volatile修饰的,因为要保证其可见性,同时也要保证字段是可访问的,如果是更新方法compareAndSet()和字段在同一个类中,字段可以是protected和private的,否则必须public的。

另外两种实现:
AtomicLongFieldUpdater长整型字段原子更新器
AtomicReferenceFieldUpdater引用字段原子更新器

AtomicStampedReference实现分析

AtomicStampedReference是避免ABA问题的引用类型,内部布局:

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;//Pair实例 volatile可见性

    //构造方法
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
    
 // 省略代码... ....
 /**
  * @param expectedReference 期望原值
  * @param newReference 新值
  * @param expectedStamp 原版本号
  * @param 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) ||
         casPair(current, Pair.of(newReference, newStamp)));
   }
}

内部类Pair封装了引用对象reference和版本号stamp。
如果期望值expectedReference等于当前值current.reference并且期望版本号expectedStamp等于当前版本号current.stamp才会往下进行,否则,直接返回false,设置失败。

如果当新值newReference等于当前值current.reference新版本号newStamp等于当前版本号current.stamp,也没必要CAS操。

其他类型原子变量的实现方式都是使用Unsafe对象进行compareAndSwap操作,不一一累述。

小结

1:CAS作为硬件同步原语,相比基于线程互斥的同步操作要更加轻量级
2:volatile的可见性加上CAS原子操作是J.U.C无锁并发实现的基础
3:原子变量可以使变量在无锁的情况下保持原子性。

码字不易,转载请保留原文连接http://www.jianshu.com/p/864b2786ec99

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,468评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,620评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,427评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,160评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,197评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,334评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,775评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,444评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,628评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,459评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,508评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,210评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,767评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,850评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,076评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,627评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,196评论 2 341

推荐阅读更多精彩内容