java多线程——CAS

Wesley13
• 阅读 512

关于无锁队列,网上有很多介绍了,我做一个梳理,从它是什么再到它有哪些特性以及应用做一个总结,方便自己使用和记录。

本文主要内容:

非阻塞同步是什么

cas是什么

特性

ABA问题

无阻塞队列

1

非阻塞同步

互斥同步属于一种悲观的并发策略,总认为只要不去做正确的同步措施,肯定会出问题,无论共享数据是否真的会出现竞争,它都要进行加锁。

而基于冲突检测的乐观并发策略,是先进行操作,如果没有竞争,就操作成功了,如果有竞争,产生冲突了,就采用补救措施,常见的就是不断的重试。

CAS就是一种乐观并发策略,除了CAS以外,还有

  • Test-and-Set(测试并设置),

  • Fetch-and-Increment(获取并增加),

  • Swap(交换),

  • LL/SC(加载链接/条件存储)

以上这些都需要硬件指令集的支持才具备原子性。比如在IA64、x86 CPU架构下可以通过cmpxchg指令完成CAS功能,而在ARM和PowerPC架构下,需要ldrex/strex指令来完成LL/SC的功能。

2

CAS是什么

cas全称为Compare-and-Swap(比较并交换),有3个操作数,分别是内存位置V、旧的的预期值A、新值B,那么当且仅当V符合旧的预期值A时,处理器用新值B更新V的值为B。

这些处理过程有指令集的支持,因此看似读-写-改操作只是一个原子操作,所以不存在线程安全问题。我们看个cas的操作过程伪代码:

int value;

int compareAndSwap(int oldValue,int newValue){

    int old_reg_value =value;

    if (old_reg_value==old_reg_value)

        value=newValue;

    return old_reg_value;

}

当多个线程尝试使用CAS同时更新同一个变量的时候,只有其中一个线程能够更新变量的值。当其他线程失败后,不会像获取锁一样被挂起,而是可以再次尝试,或者不进行任何操作,这种灵活性就大大减少了锁活跃性风险。

3

CAS特性

我们知道采用锁对共享数据进行处理的话,当多个线程竞争的时候,都需要进行加锁,没有拿到锁的线程会被阻塞,以及唤醒,这些都需要用户态到核心态的转换,这个代价对阻塞线程来说代价还是蛮高的,那cas是采用无锁乐观方式进行竞争,性能上要比锁更高些才是,为何不对锁竞争方式进行替换?

要回答这个问题,我们先举个例子。

当你开车在上班高峰期的时候,如果通过交通信号灯来控制车流,可以实现更高的吞吐量,而环岛虽然无红绿灯让你等待,但你一圈不一定能绕出你先出去的那个路口,有时候可能得多走几圈,而在低拥堵的时候,环岛则能实现更高的吞吐量,你一次就可以成功,而红路灯反而效率低下了,即便人不多,你依然需要等待。

这个例子依然适应锁和cas的比较,在高度竞争的情况下,锁的性能将超过cas的性能,但在中低程度的竞争情况下,cas性能将超过锁的性能。多数情况下,资源竞争一般都不会那么激烈。

4

非阻塞无锁链表

我们参考一个ConcurrentLinkedQueue 的源码实现,来看下cas的应用。

ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列,它是个单向链表,每个链接节点都拥有一个当前节点的元素和下一个节点的指针。

 Node {

    volatile E item;

    volatile Node next;

}

它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部(tail),当我们获取一个元素时,它会返回队列头部(head)的元素。tail节点和head节点方便我们快速定位最后一个和第一个元素。

我们看下添加一个元素的源码实现:

public boolean offer(E e) {

    checkNotNull(e);

    final Node newNode = new Node(e);

   //从tail执向的节点开始循环,查找尾部节点,

   //然后插入,直到插入成功。

    for (Node t = tail, p = t;;) {

        Node q = p.next;

        if (q == null) {

            // p 是最后一个节点

            if (p.casNext(null, newNode)) {

                if (p != t)

                    casTail(t, newNode);  // 更新tail节点指针指向newNode

                return true;

            }

            // 没有竞争上cas的线程,继续循环

        }

        else if (p == q)

            p = (t != (t = tail)) ? t : head;

        else

            p = (p != t && t != (t = tail)) ? t : q;

    }

}

上述代码主要做了如下功能:

1、从tail指针指向的节点开始循环,查找尾节点,尾节点的特征是next为null。

2、如果当前节点的nextNode!=null,则继续查找nextNode的nextNode。

3、如果当前节点的nextNode==null,表明找到尾部节点,则添加newNode到尾部节点的nextNode。

4、更新tail指针,一般指向最新尾节点.

5、如果tail节点nextNode==null,则不更新,表明上一次已经指向最新的尾node。

6、如果!=null,则更新为newNode,2次插入操作更新一次

示图:

java多线程——CAS

java多线程——CAS

java多线程——CAS

上面的代码算法过于复杂,简化如下:

while (true){

    //添加尾节点的next节点,成功之后,更新tail的指针指向最新尾节点

    if (tail.casNext(null, newNode)) {

        casTail(tail, newNode); 

        return true;

    }

}

为何要2次插入node之后,再更新tail的指针?

1、减少tail的写入次数,从而减小write开销

2、tail的读次数增加不会影响性能,虽然增加一次循环开销,但相对于写来说并不大。

3、tail加快入队效率,不会每次入队都从head开始找尾部node

有两次CAS操作,如何保证一致性?

1、如果第一个cas更新成功,第二个失败,那么对了tail会出现不一致的情况。而且即便是都更新成功了,在执行两个cas之间,仍然可能有另外一个线程会访问这个队列,那么如何保证这种多线程情况下不会出错。

2、对于第一个问题,即便tail更新失败,上述代码也会循环的找到真正的尾节点,在这里不是强制要求以tail为尾节点,它只是一个靠近尾节点的指针。

3、第二种情况,如果线程B抵达时候,发现线程A正在执行更新,那么B线程会通过反复循环来检查队列的状态,直到A完成更新,B线程又拿到了nextNode最新信息,添加新的node,从而使两个线程不会相互干扰。

5

ABA问题

尽快CAS看起来很完美,但从语义上来说并不是完美的,存在这样一个逻辑漏洞:

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它依然是A值,那么我们就认定它没有改变过。如果在这期间它的值被改为B,后来又改为A,那么CAS就会误认为它从来没有被改变过,这个漏洞也被成为“ABA”问题。

在c的内存管理机制中广泛使用的内存重用机制,如果是cas更新的是指针,机会出现一些指针错乱的问题。常见的ABA问题解决方式,就是在更新的时候增加一个版本号,每次更新之后版本号+1,从而保证数据一致。

不过大部分情况下ABA问题都不会影响到程序的正确性,如果需要解决,可以特殊考虑下,或者采用传统的互斥同步会更好。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这