原子语义同步的底层实现

volatile

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

引子

参见如下代码

package org.go;

public class Go {

	volatile int i = 0;

	private void inc() {
i++;
} public static void main(String[] args) {
Go go = new Go();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++)
go.inc();
}).start();
}
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(go.i);
}
}

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

package org.go;

import java.util.concurrent.atomic.AtomicInteger;

public class Go {

	AtomicInteger i = new AtomicInteger(0);

	private void inc() {
i.getAndIncrement();
} public static void main(String[] args) {
Go go = new Go();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++)
go.inc();
}).start();
}
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(go.i);
}
}

这时,如果你不了解 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

private static final Unsafe unsafe = Unsafe.getUnsafe();
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
} public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

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

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

Unsafe

Unsafe.java

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

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

natUnsafe.cc

jboolean
sun::misc::Unsafe::compareAndSwapInt (jobject obj, jlong offset,
jint expect, jint update)
{
jint *addr = (jint *)((char *)obj + offset);
return compareAndSwap (addr, expect, update);
} static inline bool
compareAndSwap (volatile jint *addr, jint old, jint new_val)
{
jboolean result = false;
spinlock lock;
if ((result = (*addr == old)))
*addr = new_val;
return result;
}

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

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

spinlock

什么是spinlock

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

natUnsafe.cc

class spinlock
{
static volatile obj_addr_t lock; public: spinlock ()
{
while (! compare_and_swap (&lock, 0, 1))
_Jv_ThreadYield ();
}
~spinlock ()
{
release_set (&lock, 0);
}
};

以一个静态变量 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

inline void
_Jv_ThreadYield (void)
{
#ifdef HAVE_SCHED_YIELD
sched_yield ();
#endif /* HAVE_SCHED_YIELD */
}

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

ia64/locks.h

typedef size_t obj_addr_t;
inline static bool
compare_and_swap(volatile obj_addr_t *addr,
obj_addr_t old,
obj_addr_t new_val)
{
return __sync_bool_compare_and_swap (addr, old, new_val);
} inline static void
release_set(volatile obj_addr_t *addr, obj_addr_t new_val)
{
__asm__ __volatile__("" : : : "memory");
*(addr) = new_val;
}

__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++的表达式,都不是原子操作,也就是说 你在代码里:

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

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

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

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

GCC内建的原子操作

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

type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...) type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...) bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...) __sync_synchronize (...) type __sync_lock_test_and_set (type *ptr, type value, ...) 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

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

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

__asm__ [__volatile__](assembly template//汇编模板
: [output operand list]//输入列表
: [input operand list]//输出列表
: [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原子组件和同步组件

    前言 在使用多线程并发编程的时,经常会遇到对共享变量修改操作.此时我们可以选择ConcurrentHashMap,ConcurrentLinkedQueue来进行安全地存储数据.但如果单单是涉及状态的 ...

  2. 《Java核心技术卷一》笔记 多线程同步(底层实现)

    一.锁的基本原理 多个线程同时对共享的同一数据存取 ,在这种竞争条件下如果不进行同步很可能会造成数据的讹误. 例如:有一个共享变量int sum=0, 一个线程正调用 sum+=10,另一个线程正好也 ...

  3. Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  4. java两种同步机制的实现 synchronized和reentrantlock

    java两种同步机制的实现 synchronized和reentrantlock 双11加保障过去一周,趁现在有空,写一点硬货,因为在进入阿里之后工作域的原因之前很多java知识点很少用,所以记录一下 ...

  5. 支撑Java NIO 与 NodeJS的底层技术

    支撑Java NIO 与 NodeJS的底层技术 众所周知在近几个版本的Java中增加了一些对Java NIO.NIO2的支持,与此同时NodeJS技术栈中最为人称道的优势之一就是其高性能IO,那么我 ...

  6. Java并发包同步工具之Exchanger

    前言 承接上文Java并发包同步工具之Phaser,讲述了同步工具Phaser之后,搬家博客到博客园了,接着未完成的Java并发包源码探索,接下来是Java并发包提供的最后一个同步工具Exchange ...

  7. Java I/O模型及其底层原理

    Java I/O是Java基础之一,在面试中也比较常见,在这里我们尝试通过这篇文章阐述Java I/O的基础概念,帮助大家更好的理解Java I/O. 在刚开始学习Java I/O时,我很迷惑,因为网 ...

  8. Java 线程与同步的性能优化

    本文探讨的主题是,如何挖掘出Java线程和同步设施的最大性能. 1.线程池与ThreadPoolExecutor 1)线程池与ThreadPoolExecutor 线程池的实现可能有所不同,但基本概念 ...

  9. java 线程数据同步

    java 线程数据同步 由买票实例 //java线程实例 //线程数据同步 //卖票问题 //避免重复卖票 //线程 class xc1 implements Runnable{ //定义为静态,可以 ...

随机推荐

  1. LINUX 录制屏幕制作gif动态图工具peek

    sudo add-apt-repository ppa:peek-developers/stable sudo apt update sudo apt install peek linux 压缩工具改 ...

  2. admin 后台

    https://segmentfault.com/a/1190000015835976#articleHeader3https://github.com/PanJiaChen/vue-element- ...

  3. react native 环境报错

    按照react native中文网的文档安装 1.brew 管理软件 2.node 在终端启动工程后就报下面这个错误 这个错误是在RN在第一次配置环境启动一个工程的时候 ,在这过程中下载的缓存文件不完 ...

  4. mpvue 解析

    前言 mpvue是一款使用Vue.js开发微信小程序的前端框架. 总结   生命周期的理解 文档 一次前后端实践 使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为H5和小程序提供了代码复 ...

  5. 剑指Offer 5. 用两个栈实现队列 (栈)

    题目描述 用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型. 题目地址 https://www.nowcoder.com/practice/54275ddae22f4 ...

  6. sqlite数据库操作

    1.创建表 import sqlite3 conn = sqlite3.connect('C:\\Users\\Kuma\\Desktop\\spider.db') print('connect su ...

  7. 购物车存到cookie

    为什么不存session? 首先,session存在时间限制,会定期清空的,而cookie如果不主动清或者设置定期则不会清楚: session存放在服务器端,cookie存放在客户端浏览器. 购物车存 ...

  8. Python学习笔记(day23更新)

    第一章 计算机基础 1.1 硬件 计算机基本的硬件由:CPU / 内存 / 主板 / 硬盘 / 网卡 / 显卡 等组成,只有硬件但硬件之间无法进行交流和通信. 1.2 操作系统 作用:操作系统用于协同 ...

  9. noip-2006普及组-数列- 【模拟-找规律-快速幂】

    链接:https://ac.nowcoder.com/acm/contest/153/1047 来源:牛客网 题目描述 给定一个正整数k( ≤ k ≤ ),把所有k的方幂及所有有限个互不相等的k的方幂 ...

  10. Go Example--格式化字符串

    package main import ( "fmt" "os" ) type point struct { x, y int } func main() { ...