原子语义同步的底层实现

volatile

volatile只能保证变量对各个线程的可见性,但不能保证原子性。关于 Java语言 volatile 的使用方法就不多说了,我的建议是 除了 配合package java.util.concurrent.atomic 中的类库,其他情况一概别用。更多的解释 参见 这篇文章

引子

参见如下代码

  1. package org.go;
  2. public class Go {
  3. volatile int i = 0;
  4. private void inc() {
  5. i++;
  6. }
  7. public static void main(String[] args) {
  8. Go go = new Go();
  9. for (int i = 0; i < 10; i++) {
  10. new Thread(() -> {
  11. for (int j = 0; j < 1000; j++)
  12. go.inc();
  13. }).start();
  14. }
  15. while(Thread.activeCount()>1){
  16. Thread.yield();
  17. }
  18. System.out.println(go.i);
  19. }
  20. }

每次执行上述代码结果都不同,输出的数字总是小于10000.这是因为在进行inc()的时候,i++并不是原子操作。或许有些人会提议说用 synchronized 来同步inc() , 或者 用 package java.util.concurrent.locks 下的锁去控制线程同步。但它们都不如下面的解决方案:

  1. package org.go;
  2. import java.util.concurrent.atomic.AtomicInteger;
  3. public class Go {
  4. AtomicInteger i = new AtomicInteger(0);
  5. private void inc() {
  6. i.getAndIncrement();
  7. }
  8. public static void main(String[] args) {
  9. Go go = new Go();
  10. for (int i = 0; i < 10; i++) {
  11. new Thread(() -> {
  12. for (int j = 0; j < 1000; j++)
  13. go.inc();
  14. }).start();
  15. }
  16. while(Thread.activeCount()>1){
  17. Thread.yield();
  18. }
  19. System.out.println(go.i);
  20. }
  21. }

这时,如果你不了解 atomic 的实现,你一定会不屑的怀疑 说不定 AtomicInteger 底层就是使用锁来实现的所以也未必高效。那么究竟是什么,我们来看看。

原子类的内部实现

无论是AtomicInteger 或者是 ConcurrentLinkedQueue的节点类ConcurrentLinkedQueue.Node,他们都有个静态变量

private static final sun.misc.Unsafe UNSAFE;,这个类是实现原子语义的C++对象sun::misc::Unsafe的Java封装。想看看底层实现,正好我手边有gcc4.8的源代码,对照本地路径,很方便找到Github的路径,看这里

以接口 getAndIncrement()的实现举例

AtomicInteger

AtomicInteger.java

  1. private static final Unsafe unsafe = Unsafe.getUnsafe();
  2. public final int getAndIncrement() {
  3. for (;;) {
  4. int current = get();
  5. int next = current + 1;
  6. if (compareAndSet(current, next))
  7. return current;
  8. }
  9. }
  10. public final boolean compareAndSet(int expect, int update) {
  11. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  12. }

留意这个for循环,只有在compareAndSet成功时才会返回。否则就一直compareAndSet。

调用了compareAndSet实现。此处,我注意到 Oracle JDK的实现是略有不同的,如果你查看JDK下的src,你可以看到Oracle JDK是调用的Unsafe的getAndIncrement(),但我相信Oracle JDK实现Unsafe.java的时候应该也是只调用compareAndSet,因为一个compareAndSet就可以实现增加、减少、设值的原子操作了。

Unsafe

Unsafe.java

  1. public native boolean compareAndSwapInt(Object obj, long offset,
  2. int expect, int update);

通过JNI调用的C++的实现。

natUnsafe.cc

  1. jboolean
  2. sun::misc::Unsafe::compareAndSwapInt (jobject obj, jlong offset,
  3. jint expect, jint update)
  4. {
  5. jint *addr = (jint *)((char *)obj + offset);
  6. return compareAndSwap (addr, expect, update);
  7. }
  8. static inline bool
  9. compareAndSwap (volatile jint *addr, jint old, jint new_val)
  10. {
  11. jboolean result = false;
  12. spinlock lock;
  13. if ((result = (*addr == old)))
  14. *addr = new_val;
  15. return result;
  16. }

Unsafe::compareAndSwapInt调用 static 函数 compareAndSwap。而compareAndSwap又使用spinlock作为锁。这里的spinlock有LockGuard的意味,构造时加锁,析构时释放。

我们需要聚焦在spinlock里。这里是保证spinlock释放之前都是原子操作的真正实现。

spinlock

什么是spinlock

spinlock,即自旋锁,一种循环等待(busy waiting)以获取资源的锁。不同于mutex的阻塞当前线程、释放CPU资源以等待需求的资源,spinlock不会进入挂起、等待条件满足、重新竞争CPU的过程。这意味着只有在 等待锁的代价小于线程执行上下文切换的代价时,Spinlock才优于mutex

natUnsafe.cc

  1. class spinlock
  2. {
  3. static volatile obj_addr_t lock;
  4. public:
  5. spinlock ()
  6. {
  7. while (! compare_and_swap (&lock, 0, 1))
  8. _Jv_ThreadYield ();
  9. }
  10. ~spinlock ()
  11. {
  12. release_set (&lock, 0);
  13. }
  14. };

以一个静态变量 static volatile obj_addr_t lock; 作为标志位,通过C++ RAII实现一个Guard,所以所谓的锁其实是 静态成员变量obj_addr_t lock,C++中volatile 并不能保证同步,保证同步的是构造函数里调用的 compare_and_swap和一个static变量lock.这个lock变量是1的时候,就需要等;是0的时候,就通过原子操作把它置为1,表示自己获得了锁。

这里会用一个static变量实在是一个意外,如此相当于所有的无锁结构都共用同一个变量(实际就是size_t)来区分是否加锁。当这个变量置为1时,其他用到spinlock的都需要等。 为什么不在sun::misc::Unsafe添加一个私有变量 volatile obj_addr_t lock;,并作为构造参数传给spinlock?这样相当于每个UnSafe共享一个标志位,效果会不会好一些?

_Jv_ThreadYield在下面的文件里,通过系统调用sched_yield(man 2 sched_yield)让出CPU资源。宏HAVE_SCHED_YIELD在configure里定义,意味着编译时如果取消定义,spinlock就称为真正意义的自旋锁了。

posix-threads.h

  1. inline void
  2. _Jv_ThreadYield (void)
  3. {
  4. #ifdef HAVE_SCHED_YIELD
  5. sched_yield ();
  6. #endif /* HAVE_SCHED_YIELD */
  7. }

这个lock.h在不同平台有着不同的实现,我们以ia64(Intel AMD x64)平台举例,其他的实现可以在 这里 看到。

ia64/locks.h

  1. typedef size_t obj_addr_t;
  2. inline static bool
  3. compare_and_swap(volatile obj_addr_t *addr,
  4. obj_addr_t old,
  5. obj_addr_t new_val)
  6. {
  7. return __sync_bool_compare_and_swap (addr, old, new_val);
  8. }
  9. inline static void
  10. release_set(volatile obj_addr_t *addr, obj_addr_t new_val)
  11. {
  12. __asm__ __volatile__("" : : : "memory");
  13. *(addr) = new_val;
  14. }

__sync_bool_compare_and_swap 是gcc内建函数,汇编指令"memory"完成内存屏障。

  • 一般地,如果CPU硬件支持指令 cmpxchg (该指令从硬件保障原子性,毫无疑问十分高效),那么__sync_bool_compare_and_swap就应该是用cmpxchg来实现的。
  • 不支持cmpxchg的CPU架构 可以用lock指令前缀,通过锁CPU总线的方式实现。
  • 如果连lock指令都不支持,有可能通过APIC实现

总之,硬件上保证多核CPU同步,而Unsafe的实现也是尽可能的高效。GCC-java的还算高效,相信Oracle 和 OpenJDK不会更差。

原子操作 和 GCC内建的原子操作

原子操作

Java的表达式以及C++的表达式,都不是原子操作,也就是说 你在代码里:

  1. //假设i是线程间共享的变量
  2. i++;

在多线程环境下,i的访问是非原子性的,实际分成如下三个操作数:

  • 从缓存取到寄存器
  • 在寄存器加1
  • 存入缓存

编译器会改变执行的时序,因此执行结果可能并非所期望的。

GCC内建的原子操作

gcc内建了如下的原子操作,这些原子操作从4.1.2被加入。而之前,他们是使用内联的汇编实现的。

  1. type __sync_fetch_and_add (type *ptr, type value, ...)
  2. type __sync_fetch_and_sub (type *ptr, type value, ...)
  3. type __sync_fetch_and_or (type *ptr, type value, ...)
  4. type __sync_fetch_and_and (type *ptr, type value, ...)
  5. type __sync_fetch_and_xor (type *ptr, type value, ...)
  6. type __sync_fetch_and_nand (type *ptr, type value, ...)
  7. type __sync_add_and_fetch (type *ptr, type value, ...)
  8. type __sync_sub_and_fetch (type *ptr, type value, ...)
  9. type __sync_or_and_fetch (type *ptr, type value, ...)
  10. type __sync_and_and_fetch (type *ptr, type value, ...)
  11. type __sync_xor_and_fetch (type *ptr, type value, ...)
  12. type __sync_nand_and_fetch (type *ptr, type value, ...)
  13. bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
  14. type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
  15. __sync_synchronize (...)
  16. type __sync_lock_test_and_set (type *ptr, type value, ...)
  17. void __sync_lock_release (type *ptr, ...)

需要注意的是:

  • __sync_fetch_and_add__sync_add_and_fetch 的关系 对应于 i++ 和 ++i。其他类推
  • CAS的两种实现,bool版本的 如果对比oldval与ptr成功并给ptr设值newval 返回true;另一个 返回 原本*ptr的值
  • __sync_synchronize 添加一个完全的内存屏障

OpenJDK 的相关文件

下面列出一些Github上 OpenJDK9的原子操作实现,希望能帮助需要了解的人。毕竟OpenJDK比Gcc的实现应用更广泛一些。————但终究没有Oracle JDK的源码,虽然据说OpenJDK与 Oracle的源码差距很小。

AtomicInteger.java

Unsafe.java::compareAndExchangeObject

unsafe.cpp::Unsafe_CompareAndExchangeObject

oop.inline.hpp::oopDesc::atomic_compare_exchange_oop

atomic_linux_x86.hpp::Atomic::cmpxchg

  1. inline jlong Atomic::cmpxchg (jlong exchange_value, volatile jlong* dest, jlong compare_value, cmpxchg_memory_order order) {
  2. bool mp = os::is_MP();
  3. __asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"
  4. : "=a" (exchange_value)
  5. : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
  6. : "cc", "memory");
  7. return exchange_value;
  8. }

这里需要给不熟悉C/C++的Java程序员提示一下,嵌入汇编指令的格式如下

  1. __asm__ [__volatile__](assembly template//汇编模板
  2. : [output operand list]//输入列表
  3. : [input operand list]//输出列表
  4. : [clobber list])//破坏列表

汇编模板中的%1,%3,%4对应于后面的参数列表{"r" (exchange_value),"r" (dest),"r" (mp)},参数列表以逗号分隔,从0排序。输出参数放第一个冒号右边,输出参数放第二个冒号右边。"r"表示放到通用寄存器,"a"表示寄存器EAX,有"="表示用于输出(写还)。cmpxchg指令隐含使用EAX寄存器即参数%2.

其他细节就不在此罗列了,Gcc的实现是把要交换的指针传下来,对比成功后直接赋值(赋值非原子),原子性通过spinlock保证。

OpenJDK的实现是把要交换的指针传下来,直接通过汇编指令cmpxchgq赋值,原子性通过汇编指令保证。当然gcc的spinlock底层也是通过cmpxchgq保证的。

浅述 Java 并发的更多相关文章

  1. 浅谈Java并发编程系列(八)—— LockSupport原理剖析

    LockSupport 用法简介 LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现. LockSupport是用来创建锁和其他同步 ...

  2. Java并发实现线程阻塞原语LockSupport

    LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现.LockSupport是用来创建锁和其他同步类的基本线程阻塞原语. 1.Lock ...

  3. JAVA并发-同步器AQS

    什么是AQS aqs全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLo ...

  4. 浅谈Java两种并发类型——计算密集型与IO密集型

    转载:https://blog.csdn.net/u013070853/article/details/49304099 核心是可以分别独立运行程序指令的计算单元.线程是操作系统能够进行运算调度的最小 ...

  5. Java并发编程:并发容器之ConcurrentHashMap(转载)

    Java并发编程:并发容器之ConcurrentHashMap(转载) 下面这部分内容转载自: http://www.haogongju.net/art/2350374 JDK5中添加了新的concu ...

  6. Java 并发工具包 java.util.concurrent 用户指南

    1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...

  7. Java并发编程:并发容器之ConcurrentHashMap

    转载: Java并发编程:并发容器之ConcurrentHashMap JDK5中添加了新的concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的 ...

  8. Java并发编程-并发工具包(java.util.concurrent)使用指南(全)

    1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...

  9. Java并发编程:并发容器ConcurrentHashMap

    Java并发编程:并发容器之ConcurrentHashMap(转载) 下面这部分内容转载自: http://www.haogongju.net/art/2350374 JDK5中添加了新的concu ...

随机推荐

  1. C#的基础数据类型

    一.概述 C# 的类型系统是统一的,因此任何类型的值都可以按对象处理.C# 中的每个类型直接或间接地从 object 类类型派生,而 object 是所有类型的最终基类.C#的数据类型主要分为三类:值 ...

  2. ABP 框架webapi设置跨域

    1.在.WebApi项目中使用  NuGet Install-Package Microsoft.AspNet.WebApi.Cors 2.在xxxWebApiModule类中添加如下代码 publi ...

  3. UnityShader之遮挡透明

    好久没写博客了,最近在学shader,不得不说,shader真的非常美妙,我沉迷其中无法自拔= = 之前做过一个遮挡透明的功能,当物体遮挡住主角时,该物体会变成半透明显示出主角.这次同样是遮挡透明的功 ...

  4. Grass Cownoisseur[Usaco2015 Jan]

    题目描述 In an effort to better manage the grazing patterns of his cows, Farmer John has installed one-w ...

  5. vue指令v-html示例解析

    更新元素的innerHTML,不会作为vue模板编译,可用组件来代替. 在网站上动态渲染任意 HTML 是非常危险的,因为容易导致 xss攻击.只在可信内容上使用 v-html,永不用在用户提交的内容 ...

  6. 弹性布局详解——5个div让你学会弹性布局

      前  言 JRedu 在网页制作过程中,布局是我们最重要的一个环节.可以说布局的好坏直接影响到整个网页的成败!布局成,则事半功倍:布局败,则事倍功半. 随着移动互联的到来,响应式网站风靡.这也就兴 ...

  7. 重构tableview!

    昨天用字典构筑tableview简直愚蠢!不过蠢过才知道如何写出好的代码.下面将用模型重构tableview! -------------------------------------------- ...

  8. Oracle 11g完全卸载方案(注册表清理)

    1.如果数据库配置了自动存储管理(ASM),应该先删除聚集同步服务CSS(Cluster Synchronization Services). 删除CSS服务的方法是在DOS命令行中执行如下命令:   ...

  9. 搭建 keras + tensorflow

    每次搭建深度学习环境的总要搜索各类参考.  这次搭环境,自己做个记录.这样以后搭建环境就拿自己 的文章做主线索引.  (如果参照我的博客安装的,欢迎转载分享 ) 1. 安装操作系统 (ubuntu-1 ...

  10. 【每天一道算法题】时间复杂度为O(n)的排序

    有1,2,……一直到n的无序数组,求排序算法,并且要求时间复杂度为O(n),空间复杂度为O(1),使用交换,而且一次只能交换两个数. 这个是以前看到的算法题,题目不难.但是要求比较多,排序算法中,时间 ...