关于CAS等原子操作,说点别人没说的
Java中提供了原子操作,可以简单看一下AtomicInteger类中的一个典型的原子操作incrementAndGet(),表示对原子整数变量进行加操作,并返回新的值。实现如下:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
在实现incrementAndGet()操作时,由于后续要执行CAS(compare and swap,比较并交换)操作,这个操作需要对旧值与某个地址处的值进行比较,但是在Java层无法操作地址,所以只能计算出某个字段在当前类实例中的偏移,然后在HotSpot VM中根据偏移转换为对应的地址。
调用的getAndAddInt()方法的实现如下:
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
其中的compareAndSwapInt()是native方法,对应的实现如下:
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
调用的Atomic::cmpxchg()函数的实现如下:
#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;
}
如上在C++函数中内联了一段汇编程序。使用精练的汇编不但可以缩小目标代码的大小,还可以使用汇编来提高某些经常被卧调用的代码的性能。
内联汇编的基本格式如下:
__asm__ [__volatile__] (
assembler template // 汇编代码模板
: [output operand list] // 输出操作数列表
: [input operand list] // 输入操作数列表
: [clobbered register list] // 修改的寄存器列表
);
内联汇编可以将C++函数中相关信息通过输入操作数列表传送到汇编指令中,也可以通过输出操作数列表接收到由汇编指令执行后的输出值。下面详细介绍所一下Atomic::cmpxchg()函数中内联汇编的具体意思。
1、汇编代码模板:当操作系统为多核时,mp为true,此时会在cmpxhgl指令之前加一个lock前缀。因为cmpxhgl指令本身并不是原子的(cmpxhg解码为多个微指令,这些微指令加载、检查是否相等,然后根据比较结果存储或不存储新值),但是加lock前缀后就会变为原子的。cmpxhg的操作数可以是reg + reg,也可以是mem + reg,前者不需要lock,因为在同一个核上,寄存器只会有一套。只有cmpxhg mem, reg才可能会需要lock,这个lock是对多核有效的。使用的cmpxhgl指令有个后缀l,表示操作数是4字节大小。
2、输出操作数列表,=表示操作数在指令中是只写的(输出操作数),a表示将变量放入eax寄存器。在64位模式下,只有%rax可用,因为在执行内联汇编相关的指令时之前会自动保存%rax的值,这样避免重要数据丢失。
3、r表示将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个。a同样表示eax寄存器。%1就是exchange_value,%3是dest,%4就是mp。
4、在修改的寄存器列表中,cc表示编译器汇编代码会导致CPU状态位的改变,也就是eflags指示了CPU状态。这里由于执行cmpxhgl,所以会更改eflags的状态;memory告诉编译器汇编代码会读取或修改内存中某个地址存放的值。
在HotSpot的atomic.hpp中声明了许多原子操作,这些操作不但为Java层原子操作提供实现,也会在HotSpot内部经常使用。主要是因为CAS相对互斥量来说更加轻量级,效率更高,但是达到同样的目的时,实现也相对复杂了一些。下面就举几个小例子,如下:
1、CAS保证在多线程竞争下,通过指针碰撞分配TLAB
在分配TLAB时会通过CAS来保证并发安全。实际上采用CAS配合上失败重试的方式保证更新操作的原子性,如下:
inline HeapWord* ContiguousSpace::par_allocate_impl(
size_t size,
HeapWord* const end_value
) {
do {
HeapWord* obj = top();
// 当前的空闲空间足够分配时尝试分配
if (pointer_delta(end_value, obj) >= size) {
HeapWord* new_top = obj + size;
HeapWord* result = (HeapWord*)Atomic::cmpxchg_ptr(new_top, top_addr(), obj);
if (result == obj) {
return obj; // 分配成功时返回,否则继续循环
}
} else {
return NULL; // 没有足够空间时候返回
}
} while (true);
}
2、保证一个或多个共享变量的原子操作
首先说一下,CAS只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java 1.5 开始,JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。在HotSpot VM实现轻量级锁时,也会有类似的操作。MarkWord将多个变量拼接为了一个64位数,如下:

在偏向锁的实现过程中,需要同时判断thread、epoch及biased_lock值来确定接下来的逻辑时,就将这几个数看成了一个64位的数进行了原子操作。
3、CAS实现自旋等待
在HotSpot VM内部锁Monitor的实现过程中,使用CAS进行自旋等待,以避免上下文切换。在Monitor::ILock()函数中,如果产生锁竞争,当前线程会调用Monitor::TrySpin ()进行自旋等待。这里等待时间的选取非常关键,因为如果自旋时间长则浪费CPU时间,旋转短了又不能有效避免上下文切换。其中的等待时间与Marsaglia的xor-shift算法产生的伪随机数有直接关系,有兴趣的可自行研究。
4、原子更新变量保护代码段线程安全
多线程竞争时,可以保护一段代码同一时刻只有一个线程在执行。在Monitor中有一个volatile变量,如下:
ParkEvent * volatile _OnDeck
这个变量被HotSpot VM作者标注为内部锁,也就是借助它可实现一段代码保护。
当执行一段代码时,可以通过_OnDeck将NULL设置为_LBIT,在退出时将_OnDeck再次设置为_LBIT,这样其它的CAS就又可以执行这段被保护的代码了。如下:
void Monitor::IUnlock (bool RelaxAssert) {
...
// 获取内部锁
if (CASPTR (&_OnDeck, NULL, _LBIT) != UNS(NULL)) {
return ;
}
// 确保同一时只有一个线程在执行这里的代码
// 释放内部锁
_OnDeck = NULL ;
}
CAS操作无处不在,只要用的好、用的巧,还是能极大减少互斥量的使用的。
手写Java虚拟机HotSpot已经录制一系列视频啦!有兴趣关注B站。
有对虚拟机、Java性能故障诊断与调优等感兴趣的人可以入群讨论。

关于CAS等原子操作,说点别人没说的的更多相关文章
- CAS、原子操作类的应用与浅析及Java8对其的优化
前几天刷朋友圈的时候,看到一段话:如果现在我是傻逼,那么我现在不管怎么努力,也还是傻逼,因为我现在的傻逼是由以前决定的,现在努力,是为了让以后的自己不再傻逼.话糙理不糙,如果妄想现在努力一下,马上就不 ...
- java并发编程系列二:原子操作/CAS
什么是原子操作 不可被中断的一个或者一系列操作 实现原子操作的方式 Java可以通过锁和循环CAS的方式实现原子操作 CAS( Compare And Swap ) 为什么要有CAS? Compar ...
- CAS原子操作实现无锁及性能分析
CAS原子操作实现无锁及性能分析 Author:Echo Chen(陈斌) Email:chenb19870707@gmail.com Blog:Blog.csdn.net/chen19870707 ...
- java原子操作CAS
本次内容主要讲原子操作的概念.原子操作的实现方式.CAS的使用.原理.3大问题及其解决方案,最后还讲到了JDK中经常使用到的原子操作类. 1.什么是原子操作? 所谓原子操作是指不会被线程调度机制打断的 ...
- CAS 原子操作
理会CAS和CAS: 有时候面试官面试问你的时候,会问,谈谈你对CAS的理解,这时应该有很多人,就会比较懵,当然,我也会比较懵,当然我和很多人的懵不同,很多人可能,并不知道CAS是一个什么东西,而在我 ...
- CAS机制总结
一.简介 CAS机制:(Compare and set)比较和替换 简单来说–>使用一个期望值来和当前变量的值进行比较,如果当前的变量值与我们期望的值相等,就用一个新的值来更新当前变量的值CAS ...
- Java并发指南3:并发三大问题与volatile关键字,CAS操作
本文转载自互联网,侵删 序言 先来看如下这个简单的Java类,该类中并没有使用任何的同步. 01 final class SetCheck { 02 private int a = 0; 03 ...
- JUC源码学习笔记4——原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法
JUC源码学习笔记4--原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法 volatile的原理和内存屏障参考<Java并发编程的艺术> 原子类源码基于JDK8 ...
- 锁&锁与指令原子操作的关系 & cas_Queue
锁 锁以及信号量对大部分人来说都是非常熟悉的,特别是常用的mutex.锁有很多种,互斥锁,自旋锁,读写锁,顺序锁,等等,这里就只介绍常见到的, 互斥锁 这个是最常用的,win32:CreateMute ...
- CAS无锁算法与ConcurrentLinkedQueue
CAS:Compare and Swap 比较并交换 java.util.concurrent包完全建立在CAS之上的,没有CAS就没有并发包.并发包借助了CAS无锁算法实现了区别于synchroni ...
随机推荐
- shell: xscp
#!/bin/bash ips=( 1.1.1.1 1.1.1.2 ) user= passwd= for i in ${ips[@]} do echo "== $i ==" ss ...
- [nginx]编译安装openresty
前言 OpenResty是一个基于Nginx和Lua的高性能Web平台,其内部集成了大量精良的Lua库.第三方模块以及大多数的依赖项.用于方便地搭建能够处理超高并发.扩展性极高的动态 Web 应用.W ...
- [nginx]定制http头信息
前言 修改http响应头信息,相关Nginx模块:ngx_http_headers_module expires 语法: expires [modified] time; expires [modif ...
- 在langchain中使用自定义example selector
简介 在之前的文章中,我们提到了可以在跟大模型交互的时候,给大模型提供一些具体的例子内容,方便大模型从这些内容中获取想要的答案.这种方便的机制在langchain中叫做FewShotPromptTem ...
- 使用 KubeBlocks 为 K8s 提供稳如老狗的数据库服务
原文链接:https://forum.laf.run/d/994 大家好!今天这篇文章主要向大家介绍 Sealos 的数据库服务.在 Sealos 上数据库后端服务由 KubeBlocks 提供,为用 ...
- module.exports和exports,应该用哪个
在 Node.js 编程中,模块是独立的功能单元,可以在项目间共享和重用.作为开发人员,模块让我们的生活更轻松,因为我们可以使用模块来增强应用程序的功能,而无需亲自编写.它们还允许我们组织和解耦代码, ...
- 小白python和pycharm安装大佬勿扰
编程语言发展和Python安装 计算机语言的发展 机器语言 1946年2月14日,世界上第一台计算机ENIAC诞生,使用的是最原始的穿孔卡片.这种卡片上使用的语言是只有专家才能理解的语言,与人类语言差 ...
- 给DataTable添加额外字段
//dt为DataTable dt.Columns.Add("字段名");//创建字段 //给新增字段赋值 foreach(DataRow item in dt.Rows) { i ...
- 从壹开始前后端开发【.Net6+Vue3】(二)前端框架
项目名称:KeepGoing(继续前进) 介绍 工作后,学习的脚步一直停停走走,希望可以以此项目为基础,可以不断的迫使自己不断的学习以及成长 将以Girvs框架为基础,从壹开始二次开发一个前后端管理框 ...
- UI自动化项目1说明 | 网页计算器自动化测试项目
需求: 1.对网页计算器, 进行加法的测试操作. 通过读取数据文件中的数据来执行用例. 2.网址: http://cal.apple886.com/ 测试点: 1.加法:1+1=2 2+9!=10 . ...