5.CAS以及相关的底层实现

前段时间找工作- -,一直没有什么时间写博客,断更了好长时间,不好意思了。

1.锁带来的问题

 前面在3.Java并发synchronized关键字解析中提到了synchronized实现的原理是通过对象的对象头中的标记来实现的,而且如果出现多线程争夺锁的时候后面还有一些系列的所膨胀的过程。这些过程都是比较消耗性能的。因此可以通过CAS来实现无锁的方式处理并发访问的问题。

2.CAS

 CAS是CompareAndSwap的简写,就是比较交换的意思。用一段代码来说明

    public static void casDemo(int expect,int update){
        while (original==expect){
            original=update;
        }
    }

 这里的expect是期望值,update是想要赋予的新的值,其中判断中的original表示的是原始值。大概的意思就是,如果期望值等于原始值,那么就将原始值设置成要更新的值。
 示意图


这里的old要保证可见性

 在Java中大量的用到了CAS操作,其中CAS操作是在Unsafe类中定义的,这个类在java9以后貌似换到别的实现类了。不过现在大部分都是使用的java8。在juc包下的atomic包中都使用到了Unsafe,在AQS跟其他类中也大量使用到了Unsafe的cas操作。

3.CAS的ABA问题

 关于CAS的ABA问题网上有很多描述,这里就借用一篇文章来简单说明ABA问题。但是对于ABA问题,对于不同的场景其产生的影响可能重要也可能不重要。

4.CAS的底层实现

 在JVM中跟计算机中CAS的指令执行是原子性的,那么怎么保证这个原子性的实现的呢。这里我们先进入JVM源码进行查看,这里用Unsafe类的compareAndSwapInt方法进行举例。

@HotSpotIntrinsicCandidate
public final native int compareAndExchangeInt(Object o, long offset,
                                              int expected, int x);

 其中HotSpotIntrinsicCandidate这个表情的作用是根据jdk运行的平台,比如64位32位和其他信息,来选择最佳的底层实现方式。
 进入到jvm的源码中

#define ADR "J"

#define LANG "Ljava/lang/"

#define OBJ LANG "Object;"
#define CLS LANG "Class;"
#define FLD LANG "reflect/Field;"
#define THR LANG "Throwable;"

#define DC_Args  LANG "String;[BII" LANG "ClassLoader;" "Ljava/security/ProtectionDomain;"
#define DAC_Args CLS "[B[" OBJ

#define CC (char*)  /*cast a literal from (const char*)*/
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f)

static JNINativeMethod jdk_internal_misc_Unsafe_methods[] = {
 ......

    {CC "allocateMemory0",    CC "(J)" ADR,              FN_PTR(Unsafe_AllocateMemory0)},
    {CC "reallocateMemory0",  CC "(" ADR "J)" ADR,       FN_PTR(Unsafe_ReallocateMemory0)},
    {CC "freeMemory0",        CC "(" ADR ")V",           FN_PTR(Unsafe_FreeMemory0)},
......

   {CC "compareAndSetInt",   CC "(" OBJ "J""I""I"")Z",  FN_PTR(Unsafe_CompareAndSetInt)},
    {CC "compareAndSetLong",  CC "(" OBJ "J""J""J"")Z",  FN_PTR(Unsafe_CompareAndSetLong)},
    {CC "compareAndExchangeReference", CC "(" OBJ "J" OBJ "" OBJ ")" OBJ, FN_PTR(Unsafe_CompareAndExchangeReference)},
    {CC "compareAndExchangeInt",  CC "(" OBJ "J""I""I"")I", FN_PTR(Unsafe_CompareAndExchangeInt)},
    {CC "compareAndExchangeLong", CC "(" OBJ "J""J""J"")J", FN_PTR(Unsafe_CompareAndExchangeLong)},
......
};

在jvm的unsafe.cpp文件中声明了很多java中的字段,比如用LANG表示“Ljava/lang/”,用OBJ表示“Object”等。还声明了很多跟java中Unsafe类中方法相关的方法数组。其中可以看到unsafe类中的compareAndExchangeInt方法关联到的方法是Unsafe_CompareAndSetInt,接下来进入这个方法

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) {
  //获取obj中的oop对象,这里的oop是对象结构中的一部分
  oop p = JNIHandles::resolve(obj);
  if (p == NULL) {
    volatile jint* addr = (volatile jint*)index_oop_from_field_offset_long(p, offset);
//如果以前的值与比较值匹配,则原子地在一个地址上比较并交换一个新值。
    return RawAccess<>::atomic_cmpxchg(x, addr, e) == e;
  } else {
    assert_field_offset_sane(p, offset);
  //如果以前的值与比较值匹配,则在内部指针地址原子地比较并交换一个新值。
    return HeapAccess<>::atomic_cmpxchg_at(x, p, (ptrdiff_t)offset, e) == e;
  }
} UNSAFE_END

 笔者这里对于RawAccessHeapAccess这两种CAS的操作区别不是特别清楚,然后关于oop对象大家可以在网上看看《揭秘Java虚拟机:JVM设计原理与实现》这本书里面有介绍,这里就不多介绍(其实我记得也不太清楚,只是大概记得有这么个概念),因为对C++也是不太懂- -。有明白的读者可以告知一下。但是其中index_oop_from_field_offset_longassert_field_offset_sane都是极端对应的偏移量,然后进行CAS操作。
 其中CAS的操作全部都定义在access.hpp文件中,这里进入access.hpp进行查看

// * atomic_cmpxchg: Atomically compare-and-swap a new value at an address if previous value matched the compared value.
//如果以前的值与比较值匹配,则原子地在一个地址上比较并交换一个新值。
// * atomic_cmpxchg_at: Atomically compare-and-swap a new value at an internal pointer address if previous value matched the compared value.
  //如果以前的值与比较值匹配,则在内部指针地址原子地比较并交换一个新值。

 static inline T atomic_cmpxchg(T new_value, P* addr, T compare_value) {
    verify_primitive_decorators<atomic_cmpxchg_mo_decorators>();
    return AccessInternal::atomic_cmpxchg<decorators>(new_value, addr, compare_value);
  }

  static inline T atomic_cmpxchg_at(T new_value, oop base, ptrdiff_t offset, T compare_value) {
    verify_primitive_decorators<atomic_cmpxchg_mo_decorators>();
    return AccessInternal::atomic_cmpxchg_at<decorators>(new_value, base, offset, compare_value);
  }

到了这里我已经感觉深入很难了,因为JVM的底层CAS实现并不像我们想象的那么简单,只知道最后会用到内存屏障这种机制,而内存屏障这个东西主要也是保证读写的顺序性防止指令重排。关于jvm的内存屏障种类很多的比如跟内存排序有关的屏障,跟GC有关的内存屏障。很复杂,我也不是特别的了解这里用屏障的原因,难道是为了防止读写的顺序性吗?
 在汇编层面用到的cmpxchg指令进行比较交换操作。而由于不同的cpu厂商可能有不同的指令集,而不同的操作系统实现某个操作的方式也可能不同,所以jvm根据不同的操作系统进行不同的实现,这也是java代码能够在不同的操作平台运行的原因因为在jvm层次实现了兼容。
 通过观察不同的平台的原子操作文件,发现了一个共同点。比如在windows的x86平台下原子Cmpxchg操作的实现使用如下的内嵌汇编完成的
int类型的原子交换操作

inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,
                                                T volatile* dest,
                                                T compare_value,
                                                atomic_memory_order order) const {
  STATIC_ASSERT(4 == sizeof(T));
  // alternative for InterlockedCompareExchange
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    lock cmpxchg dword ptr [edx], ecx
  }
}

long类型的原子交换操作

inline T Atomic::PlatformCmpxchg<8>::operator()(T exchange_value,
                                                T volatile* dest,
                                                T compare_value,
                                                atomic_memory_order order) const {
  STATIC_ASSERT(8 == sizeof(T));
  //计算要设置的值低4为的地址
  int32_t ex_lo  = (int32_t)exchange_value;
  //计算要设置的值高4为的地址
  int32_t ex_hi  = *( ((int32_t*)&exchange_value) + 1 );
  //计算原始值低4为的地址
  int32_t cmp_lo = (int32_t)compare_value;
//计算原始值高4为的地址
  int32_t cmp_hi = *( ((int32_t*)&compare_value) + 1 );
  __asm {
    push ebx
    push edi
    mov eax, cmp_lo
    mov edx, cmp_hi
    mov edi, dest
    mov ebx, ex_lo
    mov ecx, ex_hi
    lock cmpxchg8b qword ptr [edi]
    pop edi
    pop ebx
  }
}

 比较上面的操作发现在cmpxchgcmpxchg8b命令前面都加了一个lock修饰。这个Lock在我们前面有讲到过Java并发volatile关键字的作用和汇编原理。这里就不多说,所以可以知道原子交换的操作最后底层也是用了给总线加锁的方式,来避免数据的可见性问题的。
 这里说一下cmpxchg8b指令,这个指令是8字节的比较交换指令。因为long类型的长度是int的两倍8字节而一般的地址存储单元是4字节的,所以这里不是cmpxchg这个普通的4字节的比较交换指令。
 接下来继续看Linux平台的原子比较交换的实现,这里只是截取部分代码,没有将不同cpu的实现表现出来

inline T Atomic::PlatformCmpxchg<1>::operator()(T exchange_value,
                                                T volatile* dest,
                                                T compare_value,
                                                atomic_memory_order /* order */) const {
  STATIC_ASSERT(1 == sizeof(T));
  __asm__ volatile ("lock cmpxchgb %1,(%3)"
                    : "=a" (exchange_value)
                    : "q" (exchange_value), "a" (compare_value), "r" (dest)
                    : "cc", "memory");
  return exchange_value;
}

template<>
template<typename T>
inline T Atomic::PlatformCmpxchg<4>::operator()(T exchange_value,
                                                T volatile* dest,
                                                T compare_value,
                                                atomic_memory_order /* order */) const {
  STATIC_ASSERT(4 == sizeof(T));
  __asm__ volatile ("lock cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest)
                    : "cc", "memory");
  return exchange_value;
}

 可以看到这里使用的也是内嵌汇编的实现形式,在指令前面也有加了lock指令,而__asm__ volatile这个是表示不允许对该内联汇编优化。

5.总结

 在这里大概已经清楚了jdk内部的原子类使用Unsafe类中的Api实现的,而Unsafe类中的CAS操作底层又是在JVM中实现的。上面看到了在实现的过程中,

  1. 我们有看到屏障相关的东西(由于对于C++不熟悉,所以不知道具体怎么使用的),前面关于屏障也说过了,个人理解为这里是为了数据操作的顺序保证。
  2. 底层的比较交换用到的是对于的汇编指令,并且在前面加上的LOCK汇编指令,给总线加锁。

最后
这篇文章还需要深入,日后复习C++之后再看看,这里推荐一篇文章https://blog.csdn.net/lwg040814025/article/details/54645599可以瞅瞅,
😔深感无奈和自己C++不懂的难处。果然是想学好一门语言知道还需要实现其功能的语言。

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

推荐阅读更多精彩内容