Java 原子语义同步的底层实现
原子语义同步的底层实现
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
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
public native boolean compareAndSwapInt(Object obj, long offset,
int expect, int update);
通过JNI调用的C++的实现。
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。
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)平台举例,其他的实现可以在 这里 看到。
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的源码差距很小。
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 原子语义同步的底层实现的更多相关文章
- 基础篇:JAVA原子组件和同步组件
前言 在使用多线程并发编程的时,经常会遇到对共享变量修改操作.此时我们可以选择ConcurrentHashMap,ConcurrentLinkedQueue来进行安全地存储数据.但如果单单是涉及状态的 ...
- 《Java核心技术卷一》笔记 多线程同步(底层实现)
一.锁的基本原理 多个线程同时对共享的同一数据存取 ,在这种竞争条件下如果不进行同步很可能会造成数据的讹误. 例如:有一个共享变量int sum=0, 一个线程正调用 sum+=10,另一个线程正好也 ...
- Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- java两种同步机制的实现 synchronized和reentrantlock
java两种同步机制的实现 synchronized和reentrantlock 双11加保障过去一周,趁现在有空,写一点硬货,因为在进入阿里之后工作域的原因之前很多java知识点很少用,所以记录一下 ...
- 支撑Java NIO 与 NodeJS的底层技术
支撑Java NIO 与 NodeJS的底层技术 众所周知在近几个版本的Java中增加了一些对Java NIO.NIO2的支持,与此同时NodeJS技术栈中最为人称道的优势之一就是其高性能IO,那么我 ...
- Java并发包同步工具之Exchanger
前言 承接上文Java并发包同步工具之Phaser,讲述了同步工具Phaser之后,搬家博客到博客园了,接着未完成的Java并发包源码探索,接下来是Java并发包提供的最后一个同步工具Exchange ...
- Java I/O模型及其底层原理
Java I/O是Java基础之一,在面试中也比较常见,在这里我们尝试通过这篇文章阐述Java I/O的基础概念,帮助大家更好的理解Java I/O. 在刚开始学习Java I/O时,我很迷惑,因为网 ...
- Java 线程与同步的性能优化
本文探讨的主题是,如何挖掘出Java线程和同步设施的最大性能. 1.线程池与ThreadPoolExecutor 1)线程池与ThreadPoolExecutor 线程池的实现可能有所不同,但基本概念 ...
- java 线程数据同步
java 线程数据同步 由买票实例 //java线程实例 //线程数据同步 //卖票问题 //避免重复卖票 //线程 class xc1 implements Runnable{ //定义为静态,可以 ...
随机推荐
- python 如何生成好看的报告,在unittest的框架下
怎么生成报告:需要导入BeautifulReport import BeautifulReport as bf import unittest class Test_login(unittest.Te ...
- SQL JOIN语法,以及JOIN where 和and区别,还有where和join效率问题。
语法 join 用于根据两个或多个表中的列之间的关系,从这些表中查询数据. Join 和 Key 有时为了得到完整的结果,我们需要从两个或更多的表中获取结果.我们就需要执行 join. 数据库中的表可 ...
- Shell 变量替换及测试
声明:$ 后面跟linux可执行命令 一.变量替换 语法 说明 ${变量名#匹配规则} 从变量的开头进行规则匹配,将符合最 ...
- 团队-爬虫豆瓣top250项目-模块开发过程
项目托管平台地址:https://github.com/gengwenhao/GetTop250.git 开发模块功能: "get_info()单个页面的爬取"功能,开发时间:15 ...
- windows2008 转 centos7 数据磁盘NTFS无损挂载
转换时 原win硬盘里作为系统稳盘的硬盘必须重新格式化才能装机 数据盘在安装ntfs-3g可以直接挂载 几个重要命令: #lsblk //查看硬盘情况 df -T 只可以查看已经挂载的分区和文件系统 ...
- HTML和CSS标签常用命名规则
1.Images 存放一些网站常用的图片: 2.Css 存放一些CSS文件: 3.Flash 存放一些Flash文件: 4.PSD 存放一些PSD源文件: 5.Temp 存放所有临时图片和其它文件: ...
- 剑指Offer 21. 栈的压入、弹出序列 (栈)
题目描述 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序.假设压入栈的所有数字均不相等.例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压 ...
- ecmall 学习记录
1. /* 载入配置项 */ $setting =& af(MODULE); Conf::load($setting->getAll()); af方法定义如下: /** * 获取数组文件 ...
- httpd does not appear to be running and proxying cobbler, or SELinux is in the way.
当我们执行cobbler check时,出现这种错误:httpd does not appear to be running and proxying cobbler, or SELinux is i ...
- CC攻击原理及防范方法
一. CC攻击的原理: CC攻击的原理就是攻击者控制某些主机不停地发大量数据包给对方服务器造成服务器资源耗尽,一直到宕机崩溃.CC主要是用来消耗服务器资源的,每个人都有这样的体验:当一个网页访问的人数 ...