java多线程——CAS

这是多线程系列第五篇,其余请关注如下:java

java 多线程—线程怎么来的?node

java多线程-内存模型算法

java多线程——volatile安全

java多线程——锁多线程

关于无锁队列,网上有不少介绍了,我作一个梳理,从它是什么再到有哪些特性以及应用作一个总结,方便本身记录和使用。架构

本文主要内容:并发

  1. 非阻塞同步是什么
  2. CAS是什么
  3. CAS特性
  4. 无阻塞队列
  5. ABA问题

 

1、非阻塞同步

 

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

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

CAS就是一种乐观并发策略,除了CAS之外,还有:测试

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

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

  • Swap(交换),

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

以上这些都须要硬件指令集的支持才具有原子性。好比在IA6四、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同时更新同一个变量的时候,只有其中一个线程可以更新变量的值。当其余线程失败后,不会像获取锁同样被挂起,而是能够再次尝试,或者不进行任何操做,这种灵活性就大大减小了锁活跃性风险。

 

jvm对CAS支持

 

jdk在1.5以前没有对cas的支持,从jdk1.5开始开始引入了底层的支持,目前在int、long和对象的引用上都公开了cas操做,主要有sum.misc.Unsafe 来包装,而后由虚拟机对这些方法作特殊处理。在支持cas的平台上,运行时将其编译为相应的机器指令,最坏状况下,若是不支持cas指令,jvm会使用自旋锁来代替。

目前java对cas支持的类主要在util.concurrent.atomic 包下面,具体的使用不在介绍了,好比,咱们常见的AtomicInteger就是采用cas操做保证了int值改变的安全性。

 

3、CAS特性

 

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

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

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

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

 

4、非阻塞无锁链表

 

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

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

 Node<E> {

    volatile E item;

    volatile Node<E> next;

}

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

 

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

public boolean offer(E e) {

    checkNotNull(e);

    final Node<E> newNode = new Node<E>(e);

   

   //从tail执向的节点开始循环,查找尾部节点,而后插入,直到插入成功。

    for (Node<E> t = tail, p = t;;) {

        Node<E> q = p.next;

        if (q == null) {

            // p 是最后一个节点

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

                // 添加下一个节点成功以后,若是当前tail节点的next节点!=null,更新tail的指针指向newNode

                // 若是==null,则不更新t  

                if (p != t)

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

                return true;

            }

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

        }

        else if (p == q)

            // 若是next节点指向本身,表示tail指针自引用了,当前只有head一个节点,下一个节点从head开始循环

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

        else

            // 若是tail节点的next节点不为null,继续查找尾部节点(尾部节点的next==null)

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

    }

}

 

上述代码主要作了以下功能:

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

二、若是当前节点的nextNode!=null,则继续查找nextNode的nextNode。

二、若是当前节点的nextNode==null,代表找到尾部节点,则添加newNode到尾部节点的nextNode。

三、更新tail指针,通常指向最新尾节点

四、若是tail节点nextNode==null,则不更新,代表上一次已经指向最新的尾node。

五、若是!=null,则更新为newNode,2次插入操做更新一次

示图:

 

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

while (true){

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

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

        casTail(tail, newNode); 

        return true;

    }

}

 

为什么要2次插入node以后,再更新tail的指针?

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

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

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

 

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

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

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

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

以上就是ConcurrentLinkedQueue无阻塞链表的基本思想,咱们能够看到如何运用cas来进行共享数据进行更新,以及如何提高效率。源码采用jdk1.8。

 

5、ABA 问题

 

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

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

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

不过大部分状况下ABA问题都不会影响到程序的正确性,若是须要解决,能够特殊考虑下,或者采用传统的互斥同步会更好。

 

-----------------------------------------------------------------------------

想看更多有趣原创的技术文章,扫描关注公众号。

关注我的成长和游戏研发,推进国内游戏社区的成长与进步。

相关文章
相关标签/搜索