面试必问的 CAS ,要多了解

转载自 面试必问的 CAS ,要多了解

前言

CAS(Compare and Swap),即比较并替换,实现并发算法时常用到的一种技术,Doug lea大神在java同步器中大量使用了CAS技术,鬼斧神工的实现了多线程执行的安全性。

CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。

问题

一个n++的问题。

1
2
3
4
5
6
7
8
publicclass Case {
    publicvolatile int n;
    publicvoid add() {
        n++;
    }
}

通过javap -verbose Case看看add方法的字节码指令

1
2
3
4
5
6
7
8
9
10
11
publicvoid add();
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0      
         1: dup          
         2: getfield      #2                 // Field n:I
         5: iconst_1     
         6: iadd         
         7: putfield      #2                 // Field n:I
        10:return

n++被拆分成了几个指令:

  1. 执行getfield拿到原始n;
  2. 执行iadd进行加1操作;
  3. 执行putfield写把累加后的值写回n;

通过volatile修饰的变量可以保证线程之间的可见性,但并不能保证这3个指令的原子执行,在多线程并发执行下,无法做到线程安全,得到正确的结果,那么应该如何解决呢?

如何解决

在add方法加上synchronized修饰解决。

1
2
3
4
5
6
7
8
publicclass Case {
    publicvolatile int n;
    publicsynchronized void add() {
        n++;
    }
}

这个方案当然可行,但是性能上差了点,还有其它方案么?

再来看一段代码

1
2
3
4
5
6
7
8
publicint a = 1;
publicboolean compareAndSwapInt(intb) {
    if(a == 1) {
        a = b;
        returntrue;
    }
    returnfalse;
}

如果这段代码在并发下执行,会发生什么?

假设线程1和线程2都过了a==1的检测,都准备执行对a进行赋值,结果就是两个线程同时修改了变量a,显然这种结果是无法符合预期的,无法确定a的最终值。

解决方法也同样暴力,在compareAndSwapInt方法加锁同步,变成一个原子操作,同一时刻只有一个线程才能修改变量a。

除了低性能的加锁方案,我们还可以使用JDK自带的CAS方案,在CAS中,比较和替换是一组原子操作,不会被外部打断,且在性能上更占有优势。

下面以AtomicInteger的实现为例,分析一下CAS是如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
publicclass AtomicInteger extendsNumber implementsjava.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    privatestatic final Unsafe unsafe = Unsafe.getUnsafe();
    privatestatic final long valueOffset;
    static{
        try{
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        }catch(Exception ex) { thrownew Error(ex); }
    }
    privatevolatile int value;
    publicfinal int get() {returnvalue;}
}
  1. Unsafe,是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。
  2. 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
  3. 变量value用volatile修饰,保证了多线程之间的内存可见性。

看看AtomicInteger如何实现并发下的累加操作:

1
2
3
4
5
6
7
8
9
10
11
12
publicfinal int getAndAdd(intdelta) {   
    returnunsafe.getAndAddInt(this, valueOffset, delta);
}
//unsafe.getAndAddInt
publicfinal int getAndAddInt(Object var1, longvar2, intvar4) {
    intvar5;
    do{
        var5 = this.getIntVolatile(var1, var2);
    }while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    returnvar5;
}

假设线程A和线程B同时执行getAndAdd操作(分别跑在不同CPU上):

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程A和线程B各自持有一份value的副本,值为3。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  3. 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,运气好,线程B没有被挂起,并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为2。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值(3)和内存的值(2)不一致,说明该值已经被其它线程提前修改过了,那只能重新来一遍了。
  5. 重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

整个过程中,利用CAS保证了对于value的修改的并发安全,继续深入看看Unsafe类中的compareAndSwapInt方法实现。

1
publicfinal native boolean compareAndSwapInt(Object paramObject, longparamLong, intparamInt1, intparamInt2);

Unsafe类中的compareAndSwapInt,是一个本地方法,该方法的实现位于unsafe.cpp中

1
2
3
4
5
6
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
  1. 先想办法拿到变量value在内存中的地址。
  2. 通过Atomic::cmpxchg实现比较替换,其中参数x是即将更新的值,参数e是原内存的值。

如果是Linux的x86,Atomic::cmpxchg方法的实现如下:

1
2
3
4
5
6
7
8
inline jint Atomic::cmpxchg (jint exchange_value, volatilejint* dest, jint compare_value) {
  intmp = 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");
  returnexchange_value;
}

看到这汇编,内心崩溃

__asm__表示汇编的开始
volatile表示禁止编译器优化
LOCK_IF_MP是个内联函数

1
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

Window的x86实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inline jint Atomic::cmpxchg (jint exchange_value, volatilejint* dest, jint compare_value) {
    intmp = os::isMP(); //判断是否是多处理器
    _asm {
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}
// 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:

LOCK_IF_MP根据当前系统是否为多核处理器决定是否为cmpxchg指令添加lock前缀。

  1. 如果是多处理器,为cmpxchg指令添加lock前缀。
  2. 反之,就省略lock前缀。(单处理器会不需要lock前缀提供的内存屏障效果)

intel手册对lock前缀的说明如下:

  1. 确保后续指令执行的原子性。
  2. 在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
  3. 禁止该指令与前面和后面的读写指令重排序。
  4. 把写缓冲区的所有数据刷新到内存中。

上面的第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。

CAS缺点

CAS存在一个很明显的问题,即ABA问题。

问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?

如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。针对这种情况,java并发包中提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/330058.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 对于千万级的大表要怎么优化?

很多人第一反应是各种切分; 我给的顺序是: 第一 优化你的sql和索引; 第二 加缓存,memcached,redis; 第三 以上都做了后,还是慢,就做主从复制或主主复制,读写分离,可以在应用层做&…

MySQL元数据库——information_schema

转自: https://www.cnblogs.com/postnull/p/6697077.html 平时使用MySQL客户端操作数据库的同学,只要稍微留神都会发现,除了我们建的库之外,还经常看到三个数据库的影子: 1. information_schema 2. performance_sche…

mysql 表字段信息从一张表迁移到另一张表_MySQL(数据库)笔记

###数据库之前通过流去操作文件保存数据库的弊端:1.执行效率低2.开发成本高3.一般只能保存小量数据4.只能保存文本数据####什么是DB- DataBase 数据库:代表文件集合####什么是DBMS- DataBaseManagementSystem 数据库管理系统(软件),用于管理保存数据的文件集合,用于和程序员进行…

GET与POST传递数据的最大长度能够达到多少

各种web开发语言中,各个页面之间基本都会进行数据的传递,web开发里面比较常用的数据传递方式有get post,一直以来我都只知道get传递的数据量要比post传递的数据量要少,所以传递大数据量还是要用post,但是 get post 这两…

maven命令实战

【1】 创建maven项目 1)目录结构 mavenhello09|---src|---|---main|---|---|---java|---|---|---resources|---|---test|---|---|---java|---|---|---resources|---pom.xml 目录结构说明: main/java:主程序;main/resources&…

Mac 环境变量配置

环境变量配置 cd ~ (回到主目录home)如果你是第一次配置环境变量,可以使用“touch .bash_profile” 创建一个.bash_profile的隐藏配置文件vim .bash_profile,写入相应的环境变量,如下: # golang配置 export GOROOT/usr/local/Ce…

python测试开发面试题_python测试开发面试之深浅拷贝

先来道题热热身 a (a, b,c) c copy.copy(a) d copy.deepcopy(a) if c d: print("c和d的值相等") if id(c) id(d): print("c和d的地址相等") 想想最后打印的是什么? 什么是深拷贝和浅拷贝 深拷贝,就是在对某个对象进行拷贝的时候…

linux虚拟机tomcat上部署web项目的常用命令

1)查看 tomcat是否在运行 ps -ef | grep tomcat ps -ef 补充:Linux中的ps命令是Process Status的缩写,ps命令用来列出系统中当前运行的那些进程。ps命令可以列出当前进程的运行情况(状态、时间等信息)。在Linux系统中…

一文理清Cookie、Session、Token

发展史 1、很久很久以前,Web 基本上就是文档的浏览而已, 既然是浏览,作为服务器, 不需要记录谁在某一段时间里都浏览了什么文档,每次请求都是一个新的HTTP协议, 就是请求加响应, 尤其是我不用记…

python中debug有什么用途_史上最方便的Python Debug工具

最近在github上冒出了一个python的debug神器PySnooper,号称在debug时可以消灭print。那么该工具有哪些优点呢,如何使用该工具呢。本文就介绍该工具的优缺点和使用方式。 前言 使用python开发过程中,总是避免不了debug。传统的debug过程大致分…

能力陷阱总结

【readme】 本文总结于《能力陷阱》,感觉非常不错,有兴趣的朋友可以看下; 【1】领导者的能力陷阱 1)改变思想从行动开始; 改变做事方式,然后才能改变自己的思考方式; 2)要先在行…

Java HashMap 遍历方式性能探讨

转载自 Java HashMap 遍历方式性能探讨关于HashMap的实现这里就不展开了,具体可以参考JDK7与JDK8中HashMap的实现JDK8之前,可以使用keySet或者entrySet来遍历HashMap,JDK8中引入了map.foreach来进行遍历。原因:keySet其实是遍历了…

Raft共识算法

前提条件 Raft不考虑拜庭将军问题,即消息会延迟、丢失但不会错误。 Raft的特性 Strong leader:在 Raft 中,日志条目(log entries)只从 leader 流向其他服务器。 这简化了复制日志的管理,使得 raft 更容易…

直方图 帕累托图_如何发现现象背后的关键因素?帕累托图,质量管理的利器...

大家好:无论在日常生活还是工作中,都会发生一些事情或者结果,是我们不希望发生的。我们也希望从根本上解决掉,但有时候却无从下手,比如本来打算用来看书的时间不知道去哪里了;本来打算存款的钱也不知道去哪…

centos8上安装nginx

参考自 https://www.jianshu.com/p/9b2dd37a5af9 ; 【1】安装步骤 step1)安装nginx sudo yum install -y nginx step2)启动nginx服务 -- 开机自启动 sudo systemctl enable nginx -- 开启nginx 服务 sudo systemctl start nginx step3&am…

简单分析KafKa工作原理

架构图 Producer:Producer即生产者,消息的产生者,是消息的入口。 kafka cluster: Broker:Broker是kafka实例,每个服务器上有一个或多个kafka的实例,我们姑且认为每个broker对应一台服务器。每…

Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析

转载自 Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析网上关于 HashMap 和 ConcurrentHashMap 的文章确实不少,不过缺斤少两的文章比较多,所以才想自己也写一篇,把细节说清楚说透,尤其像 Java8 中的 ConcurrentHashMap&#…

kali mysql停止服务器_MySQL 的主从复制(高级篇)

首先要明白为什么要用 mysql 的主从复制:1–在从服务器可以执行查询工作 (即我们常说的读功能),降低主服务器压力;(主库写,从库读,降压)2–在从主服务器进行备份,避免备份期间影响主…

centos8安装并启动tomcat9

【1】 步骤如下 step1) 下载tomcat9 step2)centos8 输入 rz命令,把tomcat9 压缩包上传到centos8 没有rz命令, 安装使用 yum -y install lrzsz step3)压缩包解压到tomcat9 step4)配置jdk环境 vim /et…

unity 3d shaderlab 开发实战详解_vue实战开发011:使用router-view嵌套路由详解

前面已经把首页的顶部header和底部的footer页面写完,现在开始写内容区域了,在写内容之前我们先要将路由配置好,不然无法在页面上查看效果,所以这里我在components目录下先建了一个home.vue文件,里面简单的写了一句“我…