Java中CAS学习记录

CAS

阅读原文请访问我的博客BrightLoong's Blog

CAS在网上已经有数不清的文章,这里只是自己在学习过程中的一个记录,方便以后查阅。

一. 概述

Java中CAS全称Compare and Swap,也就是比较交换。在Java同步工具中,经常可以看到CAS的身影。在Doug Lea大神提供的J.U.C并发包中,可以说CAS是实现整个J.U.C包的基石。

在CAS方法中,有三个操作数,当前的内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相等时,将内存值V修改为B,否则什么都不做。

因为CAS会在进行修改的时候对当前内存值进行检测,所以当有其他线程修改了变量值的时候,这个时候当前线程的修改就会失败,以此来保证了“读-修改-写”操作的原子性。

三. CAS使用

先来看下面的代码:

package io.github.brightloong.lab.concurrent.cas;

import java.util.concurrent.TimeUnit;

/**
 * NoUseCAS class
 *
 * @author BrightLoong
 * @date 2018/6/10
 */
public class NoUseCAS {

    private volatile int value = 0;

    public void add() {
        value++;
    }

    public int getValue() {
        return value;
    }

    public static void main(String[] args) {
        NoUseCAS noUseCAS = new NoUseCAS();
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.MILLISECONDS.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    noUseCAS.add();
                }
            }).start();
        }
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("最后结果:" + noUseCAS.getValue());
    }
}

输出结果每次都可能不一样,而不是每次都输出10。通过volatile虽然保证了变量线程之间的可见性,但是并不能保证“++”操作的原子性,因为“++”操作是先获取到值,然后再执行“+”操作,找到NoUseCAS.class文件,执行javap -c NoUseCAS.class 得到字节码,找到“add()”方法的字节码如下:

public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field value:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field value:I
      10: return

可以看到getfield获取当前的值,iadd执行加操作,putfield赋值,如果这个时候线程A在执行完getfield后,拿到值为2,同时有另一个线程B将值修改为3,这个时候线程A继续执行操作的话最后会返回结果3,这就和期望的值不一样了。

如何解决

可以使用AtomicInteger来解决上面的问题,它提供了getAndIncrement()方法来替代“++”操作,并且保证了该操作的原子性,

代码片段:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
  
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
           //变量内存偏移地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    //使用volatile修饰保证线程间的可见性。
    private volatile int value;
    
    //原子++操作,并调用unsafe.getAndAddInt
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

}

Unsafe.java中相关代码片段如下:

//使用了compareAndSwapInt()
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
//调用本地方法(native)
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

下面具体分析本地方法

三. CAS原理

在openjdk9中找到unsafe.cpp,其路径为:jdk9u/hotspot/src/share/vm/prims/unsafe.cpp

//定义compareAndSetInt为Unsafe_CompareAndSetInt
{CC "compareAndSetInt",   CC "(" OBJ "J""I""I"")Z",  FN_PTR(Unsafe_CompareAndSetInt)},

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
  oop p = JNIHandles::resolve(obj);
  //获取内存地址
  jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);

  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
} UNSAFE_END

Atomic::cmpxchg在atomic.hpp中,文件路径为:jdk9u/hotspot/src/share/vm/runtime/atomic.hpp

inline unsigned Atomic::cmpxchg(unsigned int exchange_value,
                         volatile unsigned int* dest, unsigned int compare_value,
                         cmpxchg_memory_order order) {
  assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
  return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,
                                       (jint)compare_value, order);
}

使用的是内联函数(inline),会根据当前处理器的类型调用对应的内联函数,以下是windows_x86的实现。文件路径为:jdk9u/hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value, cmpxchg_memory_order order) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}
  • LOCK_IF_MP(MP):判断前系统是否为多核处理器如果是则为cmpxchg指令添加lock前缀。

  • cmpxchg:使用cmpxchg指令

    intel手册对lock前缀的说明如下(参考:https://www.jianshu.com/p/fb6e91b013cc):

  • 确保后续指令执行的原子性。

    在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。

  • 禁止该指令与前面和后面的读写指令重排序。

  • 把写缓冲区的所有数据刷新到内存中。

CAS的ABA问题

CAS存在ABA的问题,如下图所示,线程A最开始获取的值A,到赋值前检查的时候依然是A,然后进行了赋值;但是线程A并不知道这期间有线程B将值更改为B,然后又有线程C将值改回A。


CAS

我们可以使用版本号来解决以上的问题,也就是上面的修改就会变成1A-1B-2A,就可以发现最开始是1A,但是比较的时候是2A,代表这期间被改动过。可以使用AtomicStampedReference解决ABA的问题,它就是使用版本号来标记变量来保证CAS的正确性。

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

推荐阅读更多精彩内容