前言

学习java并发始终绕不开CAS(CompareAndSet),但如果只从java的角度来学习、理解CAS,又会觉得了解的不够深入。笔者从CPU发展史和Java发展史的角度深入探索了CAS的前世今生,希望能给读者带来更多的信息输入。

CPU发展历史

1971年11月15日,intel公司推出了世界第一款CPU,名为4004,并且发布的广告内容称"集成电子——芯片上的微型可编程计算机的新纪元"。这是一款4位处理器,而为什么intel敢这么推销它的CPU呢?
20世纪40年代,计算机技术开始发展,第一台通用电子计算机ENIAC使用了17468根真空管,占地170多平米。1947年,贝尔实验室发明了晶体管技术,随后在电子工业受到关注,贝尔实验室于1954年成功研发使用晶体管线路的计算机TRADIC,该计算机使用了800个晶体管,体积3立方英尺(约0.08立方米)。
即使晶体管计算机缩小了占地面积,功耗也大大降低,但还是存在不少问题。例如:首先是集成度低,不同计算功能分立,按需组装在一起,并且服务于特定领域,例如科学计算、军事等等,其次价格昂贵并且体积庞大。
4004微处理器将所有计算功能集成在单一芯片上,开发人员在设计不同计算机系统时,可以使用同一款芯片,降低了计算机系统设计门槛,很快工业界也开始接受CPU,甚至通用汽车公司都用上了4004芯片。

随后1972年,intel发布了第二款名为8008的CPU,这款CPU是8位微处理器的先驱,随后1974年发布了著名的8080处理器,它的时钟频率高达2MHz,拥有16KB寻址空间。

1980年,intel发布了又一款划时代的产品8086微处理器,它是intel第一款16位的微处理器,寻址空间达到1MB。从8086微处理器开始,intel开始推动x86架构演进,8086的指令集架构(ISA)成为了后来x86指令集家族的基础,并延续至今。80年代末,intex开始发布著名的80x86系列32位微处理器,如80286、80386和80486,而由于商标问题,从80586开始,intel将其改名为Pentium。

20世纪80年代,为了追求更高的处理性能,精简指令集(RISC)计算架构应运而生,它通过简化指令集,减少冗余复杂指令,从而提高单条指令的运行效率。其中最有名的指令集是MIPS和SPARC。但由于intel在指令集上的延续性以及技术创新,在个人计算机领域它一直保持霸主地位。

20世纪90年代,CPU时钟频率开始极大幅度提高,intel奔腾处理器主频从1993年60MHz提高到2000年的1.5GHz。2000年代初,多核处理器开始出现,2005年,intel推出了第一款双核奔腾处理器Pentium D 820

而英国计算机公司Acorn最早于1985年推出了第一款RISC指令集的微处理器ARM1,而后1991年发布ARM6,苹果公司在Apple Newton(PDA)产品中使用了ARM610处理器。随着2007年iPhone和2008年Android的大火,智能手机进入了飞速发展阶段,ARM因此奠定了智能手机市场的霸主地位。2015年开始,ARM发布了Cortex-A35处理器,这是ARM第一款64位处理器,其指令集AArch64现在被大家熟知,而Apple的M1及之后的CPU都采用的此指令集。

Java发展历史

James Gosling(詹姆斯·高斯林),生于1955年,1983年获得美国卡内基梅隆大学计算机博士学位,1983年加入IBM工作,1984年跳槽到Sun公司。1995年主导启动Green Project(绿色计划),旨在开发一种能够在消费电子产品上运行的程序架构,并由此设计出Oak语言。

1996年,Oak语言改名为Java,并在Sunworld大会上正式发布Java 1.0版本,此时的java语法简单,只有10个关键类。
1997年2月,JDK 1.1发布,其中包含了JDBC、JavaBeans、RMI等,同时增加了内部类和反射功能。
2002年2月,JDK 1.4版本发布,此版本中技术体系划分成了三个方向,分别是面向桌面应用的J2SE,面向企业的J2EE,和面向手机等移动终端的J2M。此版本中Java虚拟机支持了JIT(Java Intime Complier)。
2004年9月30日,JDK 5发布,Sun公司放弃了 "JDK 1.x"的命名方式,JDK 5在易用性上做了很大改进,例如自动装箱、泛型、注解、枚举、可变参数、强化循环遍历等重大新语法特性。并且还引入了JUC(java.util.concurrent)包。
2006年,Sun公司开源了Java,并建立OpenJDK组织管理开源代码。Sun JDK和Open JDK除了版本注释不同外,其余代码几乎完全一致。
2009年,Oracle以74亿美元价格收购市值曾超过2000亿美元的Sun公司,Java商标由此划归Oracle。2010年James Gosling从Oracle离职。
2011年,JDK7发布,并引入了G1垃圾回收器。

处理器和Java发展史的关联

对照着处理器发展史和Java发展史,可以看出80年代到90年代是一个处理器爆发的时代。RISC架构处理器开始出现并在服务器领域挑战x86,而cpu的频率也由最早的2MHz增加到1.5GHz。虽然没法采访到James Gosling,但也能猜出当时他的想法,即创造一个在不同架构处理器上都能运行的程序,屏蔽处理器差异,提升开发人员效率。

而为什么1995年才开始着手创造新的语言呢?和CPU发展历史对照来看,也就是在1993年后,CPU运行频率大幅度提升,而为人们熟知的windows95操作系统就发布于1995年。windows95操作系统的成功和CPU运行频率的提升也有不少关系,windows95虽然能够很好的支持32位应用程序运行,但如果CPU频率不够高,人们又怎么舍得让有限的CPU跑一个图形化的操作系统呢?
回到Java,CPU运行频率显著提升让软件开发人员有了构建不同层次软件系统的基础,除开直面CPU的操作系统,jvm虚拟机也有了其生存空间。

2000年代初,多核处理器开始出现,传统的基于单核时分多路的并发执行开始向真正并行运行转变,从应用层面上来看,一个线程有机会在不退出CPU占用的前提下,获取线程间共享资源拥有权。举个例子,在单核情况下,如果A线程无法占有资源,那他只能退出时间片轮转,待B线程释放资源后,再次获得CPU资源后,判断是否能占有,并进行占有。而在多核情况下,两个线程是并行运行的,因此A线程可以一直占有一个CPU核,等待B线程在另一个CPU核中运行时释放资源。由此,java史上极为重要的创新JUC包也在此时出世,极大地为开发人员简化了并发编程。

JVM中的CAS实现

CAS(CompareAndSwap)实际上是一个统称,它指的是在CPU指令层面,一个CPU原子操作,实现比较寄存器中的某值是否为给定值,如果是,则将其更新为指定值,如果不是,则不进行操作,它的原子性由CPU保证。

我们熟知的x86指令集中就包含CAS指令,它的汇编指令名是CMPXCHG,这个指令是在80386中引入的,有人可能想问了,80386是1985年发布的,为什么95年出的java不直接支持CAS呢?个人猜测,一方面要考虑不同指令集架构的兼容性,虽然x86指令集架构支持了CAS,但其他指令集架构可能还不兼容,这个指令还没有被业界认可;另一方面,多核处理器在2000年代初才被引入,并行能力的出现使得业界开始关注锁的性能。

既然JDK是从版本5开始支持CAS的,那我们就从JDK5开始探索java cas。由于openjdk是从7开始开源的,最早的版本也是jdk7-b24,不过oracle允许以研究为目的申请源码资源,网上有大佬公布了jdk1.4、jdk5.0和jdk6u21版本的源码,大家可通过此链接前往。

另外,笔者还发现oracle提供了jdk6-b27源代码的直接下载权限,另外读者如果有兴趣,也可以
这个页面申请源码JDK5.0源码资源

接下来,笔者会分享jvm中针对不同指令集架构下的CAS实现。

JDK5.0

打开此目录,我们会看到当年的JDK5分别支持了三大系统:linux、win32和solaris。然后会看到不同的指令集架构,其中linux和win32都支持amd64、i486和ia64,solaris系统支持i486和sparc。其中sparc就是我们前面提到过的80年代开始发力的精简指令集中的一种。

由于精力有限,我们只看32位的cas,64位及其他读者有兴趣可以进一步研究。

atomic_linux_amd64.inline.hpp

inline jint Atomic::cmpxchg(jint exchange_value,
                            volatile jint* dest,
                            jint compare_value)
{
  __asm__ __volatile__ ("lock ; cmpxchgl %1,(%3)"
                        : "=a" (exchange_value)
                        : "r" (exchange_value), 
                          "a" (compare_value),
                          "r" (dest)
                        : "memory");
  return exchange_value;
}

atomic_linux_i486.inline.hpp

// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  bool mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

atomic_linux_ia64.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  __asm__ volatile (  "mf;;zxt4 %1=%3;;mov ar.ccv=%3;;cmpxchg4.acq %0=[%4],%2,ar.ccv;;sxt4 %0=%2;;"
            : "=r" (exchange_value), "=r" (compare_value)
            : "0" (exchange_value), "1" (compare_value), "r" (dest)
                    : "memory");
  return exchange_value;
}

atomic_solaris_i486.inline.hpp

extern "C" jint _Atomic_cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value, bool mp);

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  return _Atomic_cmpxchg(exchange_value, dest, compare_value, os::is_MP());
}

atomic_solaris_sparc.inline.hpp

// _LP64 || COMPILER2
extern "C" jint     _Atomic_swap32(jint     exchange_value, volatile jint*     dest);

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  return _Atomic_cas32(exchange_value, dest, compare_value);
}

// 32-bit compiler1 only
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  return (*os::atomic_cmpxchg_func)(exchange_value, dest, compare_value);
}

sparc架构在此处好像略显复杂,这一点也不奇怪,即使是solaris系统、sparc架构,不同版本的操作系统和CPU所支持的能力也略有差别,例如sparc v8架构,它是32位架构,操作系统层面支持cas,而v9架构是64位架构,此时c语言已经支持了sparcv9,因此只用引入c语言接口即可。

atomic_win32_amd64.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  return (*os::atomic_cmpxchg_func)(exchange_value, dest, compare_value);
}

atomic_win32_i486.inline.hpp

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // alternative for InterlockedCompareExchange
  bool 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   
  }
}

atomic_win32_ia64.inline.hpp

inline intptr_t Atomic::cmpxchg_ptr(intptr_t exchange_value, volatile intptr_t* dest, intptr_t compare_value) {
  return (intptr_t)cmpxchg((jlong)exchange_value, (volatile jlong*)dest, (jlong)compare_value);
}

此这些代码中可以看出,486和amd64和多核处理器已经上市,在进行cmpxchg前,需要对总线、cpu内部缓存、或原子性锁定,防止其他CPU核操作相同内存地址。
sparc作为精简指令集的代表,sun公司的当家产品,配合solaris操作系统,当时应该是企业级服务器市场的重要一员。而笔者猜测,sun公司支持java,也是希望其他操作系统处理器架构下的程序未来也能方便地迁移到solaris上,方便其增加市场份额。

JDK6

打开此目录,我们会看到支持的操作系统还是linux、windows和solaris,但不同系统对应处理器架构的代码却有了变化。

  1. 首先i486和amd64合并了,变成了x86;其次solaris不再支持x86架构处理器;
  2. linux额外支持了zero架构,zero的含义是零汇编(Zero-Assembler),使用C++代码代替汇编编码,其原因是linux支持的处理器架构很多,而jdk如果只支持sparc和x86,那局限性就很大,因此引入了zero架构,使其能够运行在非sparc和x86架构的处理器上。
  3. ia64架构消失了,ia64架构对应的处理器是itanium处理器,是intel自己研发的64位架构,由于完全不兼容32位指令集架构,市场反响并不好。2001年上市,2012年安腾9500系列发布后,安腾的研发团队也都逐渐转到了志强产品线。2021年7月29日intel正式停止安腾9700系列处理器出货,标志着安腾架构的消亡。

atomic_linux_x86.inline.hpp

// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

x86和amd64合并成了一套代码,这意味着amd64将稳定兼容x86,最终形成x86_64架构。

atomic_linux_sparc.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  jint rv;
  __asm__ volatile(
    " cas    [%2], %3, %0"
    : "=r" (rv)
    : "0" (exchange_value), "r" (dest), "r" (compare_value)
    : "memory");
  return rv;
}

sparc架构上也出现了cas原语,相较于JDK5中的64位和32位复杂判断以及C++调用,显得更加简单了。

atomic_linux_zero.inline.hpp

// m68k
static inline int __m68k_cmpxchg(int oldval, int newval, volatile int *ptr) {
  int ret;
  __asm __volatile ("cas%.l %0,%2,%1"
                   : "=d" (ret), "+m" (*(ptr))
                   : "d" (newval), "0" (oldval));
  return ret;
}

// arm
typedef int (__kernel_cmpxchg_t)(int oldval, int newval, volatile int *ptr);
#define __kernel_cmpxchg (*(__kernel_cmpxchg_t *) 0xffff0fc0)

/* Perform an atomic compare and swap: if the current value of `*PTR'
   is OLDVAL, then write NEWVAL into `*PTR'.  Return the contents of
   `*PTR' before the operation.*/
static inline int arm_compare_and_swap(volatile int *ptr,
                                       int oldval,
                                       int newval) {
  for (;;) {
      int prev = *ptr;
      if (prev != oldval)
        return prev;

      if (__kernel_cmpxchg (prev, newval, ptr) == 0)
        // Success.
        return prev;

      // We failed even though prev == oldval.  Try again.
    }
}

此处可以看到,zero架构实际上内部也是会判断不同架构,从而使用不同的cas汇编/系统调用。

atomic_solaris_sparc.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  jint rv;
  __asm__ volatile(
    " cas    [%2], %3, %0"
    : "=r" (rv)
    : "0" (exchange_value), "r" (dest), "r" (compare_value)
    : "memory");
  return rv;
}

solaris中的sparc架构实现和linux中的sparc一样,也简化了。

atomic_solaris_x86.inline.hpp

extern "C" jint _Atomic_cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value, int mp);

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  return _Atomic_cmpxchg(exchange_value, dest, compare_value, (int) os::is_MP());
}

solaris中的i486也转变为x86了。

atomic_windows_x86.inline.hpp

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:


// AMD64
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  return (*os::atomic_cmpxchg_func)(exchange_value, dest, compare_value);
}

// X86
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // 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
  }
}

可以看到linux的x86和win的x86是线上,还是有少许不同,这应该和操作系统有关系。

JDK7

打开此目录,会发现操作系统和CPU架构又有了变化。其中linux中的zero架构不见了,solaris又开始支持x86。

atomic_linux_x86.inline.hpp

// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

可以看到linux下x86架构cas功能已经稳定下来,和JDK6中的实现是一样的。

atomic_solaris_sparc.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  jint rv;
  __asm__ volatile(
    " cas    [%2], %3, %0"
    : "=r" (rv)
    : "0" (exchange_value), "r" (dest), "r" (compare_value)
    : "memory");
  return rv;
}

atomic_solaris_x86.inline.hpp

extern "C" jint _Atomic_cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value, int mp);

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  return _Atomic_cmpxchg(exchange_value, dest, compare_value, (int) os::is_MP());
}

sparc由于提供了cas指令,因此实现上也是稳定的。

atomic_windows_x86.inline.hpp

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:

// AMD64
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  return (*os::atomic_cmpxchg_func)(exchange_value, dest, compare_value);
}

// X86
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // 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
  }
}

win下的x86也和JDK6没有多大差别。

其他版本

openjdk7以上的版本中,常见架构cas实现已基本稳定,笔者在此就不具体展开了,贴出一些链接,供有兴趣的朋友进行研究。jdk8-b120jdk9-b181jdk11-b28jdk11-b35

jdk9中引入了ppc和s390架构,ppc架构是是RISC精简指令集中的一种,是IBM开发的PowerPC处理器支持的架构。s390架构是IBM开发的面向大型机应用的处理器。这里分析一下这两个架构在linux系统中cas的实现。

atomic_linux_ppc.hpp

inline void cmpxchg_pre_membar(cmpxchg_memory_order order) {
  if (order != memory_order_relaxed) {
    __asm__ __volatile__ (
      /* fence */
      strasm_sync
      );
  }
}

inline void cmpxchg_post_membar(cmpxchg_memory_order order) {
  if (order != memory_order_relaxed) {
    __asm__ __volatile__ (
      /* fence */
      strasm_sync
      );
  }
}

inline jint Atomic::cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value, cmpxchg_memory_order order) {

  // Note that cmpxchg guarantees a two-way memory barrier across
  // the cmpxchg, so it's really a a 'fence_cmpxchg_fence' if not
  // specified otherwise (see atomic.hpp).

  unsigned int old_value;
  const uint64_t zero = 0;

  cmpxchg_pre_membar(order);

  __asm__ __volatile__ (
    /* simple guard */
    "   lwz     %[old_value], 0(%[dest])                \n"
    "   cmpw    %[compare_value], %[old_value]          \n"
    "   bne-    2f                                      \n"
    /* atomic loop */
    "1:                                                 \n"
    "   lwarx   %[old_value], %[dest], %[zero]          \n"
    "   cmpw    %[compare_value], %[old_value]          \n"
    "   bne-    2f                                      \n"
    "   stwcx.  %[exchange_value], %[dest], %[zero]     \n"
    "   bne-    1b                                      \n"
    /* exit */
    "2:                                                 \n"
    /* out */
    : [old_value]       "=&r"   (old_value),
                        "=m"    (*dest)
    /* in */
    : [dest]            "b"     (dest),
      [zero]            "r"     (zero),
      [compare_value]   "r"     (compare_value),
      [exchange_value]  "r"     (exchange_value),
                        "m"     (*dest)
    /* clobber */
    : "cc",
      "memory"
    );

  cmpxchg_post_membar(order);

  return (jint) old_value;
}

可以看到,在PPC架构中,CAS的实现也用的是汇编,但操作实在复杂,这难道就是RISC精简指令集的特性?笔者实在不太理解。

atomic_linux_s390.hpp

//----------------
// Atomic::cmpxchg
//----------------
// These methods compare the value in memory with a given compare value.
// If both values compare equal, the value in memory is replaced with
// the exchange value.
//
// The value in memory is compared and replaced by using a compare-and-swap
// instruction. The instruction is NOT retried (one shot only).
//
// The return value is the (unchanged) value from memory as it was when the
// compare-and-swap instruction completed. A successful exchange operation
// is indicated by (return value == compare_value). If unsuccessful, a new
// exchange value can be calculated based on the return value which is the
// latest contents of the memory location.
//
// Inspecting the return value is the only way for the caller to determine
// if the compare-and-swap instruction was successful:
// - If return value and compare value compare equal, the compare-and-swap
//   instruction was successful and the value in memory was replaced by the
//   exchange value.
// - If return value and compare value compare unequal, the compare-and-swap
//   instruction was not successful. The value in memory was left unchanged.
//
// The s390 processors always fence before and after the csg instructions.
// Thus we ignore the memory ordering argument. The docu says: "A serialization
// function is performed before the operand is fetched and again after the
// operation is completed."

jint Atomic::cmpxchg(jint xchg_val, volatile jint* dest, jint cmp_val, cmpxchg_memory_order unused) {
  unsigned long old;

  __asm__ __volatile__ (
    "   CS       %[old],%[upd],%[mem]    \n\t" // Try to xchg upd with mem.
    // outputs
    : [old] "=&d" (old)      // Write-only, prev value irrelevant.
    , [mem] "+Q"  (*dest)    // Read/write, memory to be updated atomically.
    // inputs
    : [upd] "d"   (xchg_val)
    ,       "0"   (cmp_val)  // Read-only, initial value for [old] (operand #0).
    // clobbered
    : "cc"
  );

  return (jint)old;
}

S390架构是IBM面向大型机应用的处理器架构,其也实现了CAS指令,因此实现起来也非常简单。

在JDK11中,开始支持ARM AARCH64架构,由于此时C++11编译器已经内置了CAS函数(__atomic_compare_exchange),因此JDK也就采用了C++内置函数实现CAS。

atomic_linux_aarch64.hpp

inline T Atomic::PlatformCmpxchg<byte_size>::operator()(T exchange_value,
                                                        T volatile* dest,
                                                        T compare_value,
                                                        atomic_memory_order order) const {
  STATIC_ASSERT(byte_size == sizeof(T));
  if (order == memory_order_relaxed) {
    T value = compare_value;
    __atomic_compare_exchange(dest, &value, &exchange_value, /*weak*/false,
                              __ATOMIC_RELAXED, __ATOMIC_RELAXED);
    return value;
  } else {
    return __sync_val_compare_and_swap(dest, compare_value, exchange_value);
  }
}

而linux-arm架构中,32位架构使用了c++内置函数进行cas操作,64位架构则直接采用汇编进行cas操作。

atomic_linux_arm.hpp

inline int32_t reorder_cmpxchg_func(int32_t exchange_value,
                                    int32_t volatile* dest,
                                    int32_t compare_value) {
  // Warning:  Arguments are swapped to avoid moving them for kernel call
  return (*os::atomic_cmpxchg_func)(compare_value, exchange_value, dest);
}

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));
#ifdef AARCH64
  T rv;
  int tmp;
  __asm__ volatile(
    "1:\n\t"
    " ldaxr %w[rv], [%[dest]]\n\t"
    " cmp %w[rv], %w[cv]\n\t"
    " b.ne 2f\n\t"
    " stlxr %w[tmp], %w[ev], [%[dest]]\n\t"
    " cbnz %w[tmp], 1b\n\t"
    " b 3f\n\t"
    "2:\n\t"
    " dmb sy\n\t"
    "3:\n\t"
    : [rv] "=&r" (rv), [tmp] "=&r" (tmp)
    : [ev] "r" (exchange_value), [dest] "r" (dest), [cv] "r" (compare_value)
    : "memory");
  return rv;
#else
  return cmpxchg_using_helper<int32_t>(reorder_cmpxchg_func, exchange_value, dest, compare_value);
#endif
}

基于CAS的JUC

在HOTSPOT虚拟机中,CAS操作被封装在了 Unsafe 这个类中,JDK的作者给此类增加了诸多限制,需要通过特殊方法才能在应用中获取此类实例。显然,JDK的作者并不希望大家直接使用CAS这个原语,而是希望大家使用基于CAS的JUC并发包中的无锁并发工具,此包中包含了众多日常可能会用到的并发工具,极大地方便了开发者开发并发应用。

Atomic系列

Atomic系列主要是对基础类型及引用的包装,能够让开发者安全地跨线程使用基础类型和对象。

  • AtomicBoolean :原子性操作Boolean,例如执行当目前为true时,设置为false,成功,返回true,失败返回false。
  • AtomicInteger :原子性操作Integer,例如执行当前为0时,设置为1,成功,返回true,失败返回false;执行原子性加一/减一操作。
  • AtomicLong :同AitomiInteer,只不过包装的是Long。
  • AtomicReference :原子性操作引用,例如执行当前为A实例时,设置为B实例,成功,返回true,失败返回false。

此外还有其他一些原子性类,就不一一列举了,这些原子性类底层都是基于CAS实现的。

Lock

Lock是用来代替synchronized关键字的,Lock相较于synchronized关键字,有以下几个优点:

  • Lock的实现ReentrantLock可以选择公平锁和非公平锁
  • Lock锁提供了可中断的锁等待,可避免长时间等待
  • Lock可以提供更多的条件队列(Condition)

Lock中的公平锁和非公平锁是通过AQS(AbstractQueuedSynchronizer)实现的,通过短时间自旋,实现非阻塞获取锁,在短时间获取不了锁时,配合LockSupport中的 park 和 unpark 方法,实现线程的waiting和唤醒。

并发集合系列

针对集合,java最早提供了 Vector 代替 ArrayList 实现并发访问,提供 HashTable 代替 HashMap 实现并发访问,此外还在 java.util.Collections中提供同步方法包装非同步集合。

Collection collection = Collections.synchronizedCollection(new ArrayList<>());
List list = Collections.synchronizedList(new ArrayList<>());
Map map = Collections.synchronizedMap(new HashMap<>());
Set set = Collections.synchronizedSet(new HashSet<>());

从JDK5开始,则提供了 ArrayBlockingQueue 、 LinkedBlockingDeque 、 ConcurrentHashMap、 ConcurrentLinkedQueue 等多个并发集合类,这些类内部均基于Lock实现了无锁集合操作。

需要注意,在juc中, CopyOnWriteArrayList 和 CopyOnWriteArraySet 内部采用的是synchronized关键字,在进行写操作时,直接在内部创建新的集合,避免改动原有集合,从而实现了并发操作。

其他并发操作类

CountDownLatch

CountDownLatch允许一个或多个线程等待,直到其他线程执行完操作。其内部也使用到了AQS,在线程等待时,先自旋判断是否完成了所有countDown,多次自旋后,被 LockSupport park,直到所有countDown完成,await的线程将通过 LockSupport 的unpark方法唤醒。

// thread1
CountDownLatch countDownLatch = new CountDownLatch(2);

// thread2
// do something
countDownLatch.countDown();

// thread3
// do something
countDownLatch.countDown();


// thread1
countDownLatch.await();

Semaphore

Semaphore(信号量),用于限制对资源的访问数量,常用于实现资源池的控制,其内部也是通过AQS实现的。

Semaphore semaphore = new Semaphore(100);
semaphore.acquire(30);
try {
    // do something
}finally {
    semaphore.release(30);
}

CyclicBarrier

CyclicBarrier 允许一组线程一起等待,直到所有线程都进入到等待,可以调用reset方法重用,比CountDownLach更灵活。

CyclicBarrier barrier = new CyclicBarrier(2);

// thread 1
barrier.await();

// thread 2
barrier.await();

// 重置后,可以重新await
barrier.reset();

其他

juc中还有很多并发相关的类等待着你我的探索,笔者由于精力有限,就暂时不展开了,有兴趣的朋友可以顺着笔者的思路再多看看一些类的实现。

CAS的优劣

前面讲了这么多CAS的发展历史,应用场景,那它存在缺点吗?实际上是有的。

ABA问题

所谓ABA问题,就是一个值原来是A,变成了B,又变回了A,此时CAS是无法检查到变化。例如此前提到的 AtomicReference ,线程1将引用从A变成了B,在完成一些操作后,又将引用B改回了引用A。此时,另外一个线程在compareAndSet时,虽然成功了,但却不知道此引用被更新过两次。

此问题一般情况下不会遇到,但如果有此类需求时,可以使用 AtomicStampedReference 来实现安全引用操作。

Object origin = new Object();
AtomicStampedReference reference = new AtomicStampedReference(origin, 1);
Object newObject = new Object();
// 1为预期值,2为要修改成的值
boolean success = reference.compareAndSet(origin, newObject, 1, 2);

长时间自旋

通过本文的介绍,相信你也知道了CAS是如何实现无锁并发的,那就是需要获取锁的线程不停地执行CAS操作,直到CAS成功,这意味着这个线程会一直占据CPU资源。

而实际上java内置的AQS中,已经实现了自旋、Thread.onSpinWait()以及LockSupport.park()。

当判断当前线程没有排在前列时,从JDK9开始会调用Thread.onSpinWait()方法,让CPU获悉当前线程在自旋,从而进行更底层的调度优化。

而多次自旋无法获取锁后,将使用LockSupport.park()方法,将本线程置于waiting状态,直到可以获取锁了,再通过LockSupport.unpark(Thread thread)方法将指定线程唤醒。

c1_LIRGenerator.cpp#L3241中,可以看到onSpinWait方法对应的是on_spin_wait()方法,而on_spin_wait()方法在x86上的实现是pause指令。

vm_version_x86.hpp

// SSE2 and later processors implement a 'pause' instruction
// that can be used for efficient implementation of
// the intrinsic for java.lang.Thread.onSpinWait()
static bool supports_on_spin_wait() { return supports_sse2(); }

c1_LIRAssembler_x86.cpp

void LIR_Assembler::on_spin_wait() {
  __ pause ();
}

pause指令提高了spin-wait循环的性能,它比sleep让线程进入timed_waiting、yield让线程从runable变为new这两种行为都要好。

多个共享变量的原子操作

当对一个共享变量执行操作时,CAS能够保证原子性,但对于多个共享变量,CAS就无能为力了。此时,可以使用Lock锁,锁内临界区代码可以保证只有当前线程可以操作。

总结

从处理器发展的历史出发,结合java的发展历史,让我们从源头彻底了解了CAS的前世今生。从i386处理器引入CMPXCHG指令开始,到2000年代初的多核CPU,让我们看到了x86架构下jvm中CAS实现的变化;从jvm内置汇编跨平台实现CAS,到C++11编译器内置CAS函数,也让我们见证了C++的发展历史。

相信随着处理器的演进,C++规范的进一步发展,未来java在平台新特性的引入过程,将会越来越顺利。

参考

CPU发展史
ARM发展简史
ARM内核介绍
M1芯片介绍
硅谷传奇
零汇编
linux处理器体系架构介绍
ARM LSE介绍
C++11 __atomic_compare_exchange函数
x86 sse2 pause指令介绍
x86 sse2指令集介绍

标签: java, CAS

添加新评论