CAS基础和原子类总结

访客 阅读:121 2021-09-14 19:39:37 评论:0
本文章主要介绍了CAS基础和原子类,具有不错的的参考价值,希望对您有所帮助,如解说有误或未考虑完全的地方,请您留言指出,谢谢!

  基于CAS实现的AtomicIntegerAtomicLong、 AtomicReference、 AtomicBoolean也被称为乐观锁。

  CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

  CAS简单的过程可以描述如下:

  备份旧数据,比较内存中的数据和旧数据,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行。如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。 CAS 操作是基于共享数据不会被修改的假设,失败了之后会继续尝试修改,直至成功。

  CAS(比较并交换)是指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。CAS也是存在开销的,也被称为轻量级锁。

 

  atomic中的CAS操作都是通过sun.misc.Unsafe类来保证的,可以看看这个类的实现,支持CAS的CPU将以CAS方式实现,否则以自旋的方式实现。

  voalitate型变量可以保证多线程间修改的可见性,但原子性不能保证。例如,自增、自减、赋值类的操作使用atomic类来完成是很合适的。

  atomic类型的变量可以分为以下几大类:

  AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

  AtomicIntegerArray,AtomicLongArray

  AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

  AtomicMarkableReference,AtomicStampedReference,AtomicReferenceArray

  set( )和get( )方法:原子性操作设定和获取atomic的值。类似于volatile,保证数据会在主存中设置或读取。

  getAndSet( )方法:原子性操作将变量设定为新值,同时返回先前的旧值。

  compareAndSet( ) 和weakCompareAndSet( )方法:这2个方法有2个参数,一个是期望数据(expected),一个是新数据(new);如果atomic里面的数据和期望数据一致,则将新数据设定给atomic的数据,返回true,表明成功;否则就不设定,并返回false。

  对于AtomicInteger、AtomicLong还提供了一些特别的方法。getAndIncrement( )、incrementAndGet( )、getAndDecrement( )、decrementAndGet ( )、addAndGet( )、getAndAdd( )以实现一些加法,减法原子操作。

        

         看看AtomicInteger中getAndIncrement方法的实现:

public final int getAndIncrement() 
   { 
     while (true) 
     { 
       int i = get(); 
       int j = i + 1; 
       if (compareAndSet(i, j)) 
         return i; 
     } 
   }

  方法中加上了while (true)是因为可能存在2个线程同时执行getAndIncrement操作,则只有一个可以成功,另外一个的compareAndSet(i, j)会失败,需要继续的执行compareAndSet(i, j)。所以即使多线程环境下getAndIncrement所得到的最终结果肯定是正确的。

   使用compareAndSet( )时则需要自己考虑compareAndSet(i, j)失败的情况。下面的例子模拟了一个适用于并发的stack。

import java.util.concurrent.atomic.AtomicReference; 
 
public class ConcurrentStack<E> {  
    AtomicReference<Node<E>> atomicReference = new AtomicReference<Node<E>>();  
    public void push(E item) {  
        Node<E> newHead = new Node<E>(item);  
        Node<E> oldHead;  
        do {  
            oldHead = atomicReference.get();  
            newHead.next = oldHead;  
        } while (!atomicReference.compareAndSet(oldHead, newHead));  
    }  
    public E pop() {  
        Node<E> oldHead;  
        Node<E> newHead;  
        do {  
            oldHead = atomicReference.get();  
            if (oldHead == null)   
                return null;  
            newHead = oldHead.next;  
        } while (!atomicReference.compareAndSet(oldHead,newHead));  
        return oldHead.item;  
    }  
    static class Node<E> {  
        final E item;  
        Node<E> next;  
        public Node(E item) { this.item = item; }  
    }  
     
    public static void main(String[] args){ 
        //这里的测试应该以多线程的方式来完成 
        ConcurrentStack<String> cs = new ConcurrentStack<String>(); 
        cs.push("123"); 
        cs.push("456"); 
        cs.push("abc");     
        System.out.println(cs.pop()); 
        System.out.println(cs.pop()); 
        System.out.println(cs.pop()); 
         
        System.out.println(Runtime.getRuntime().availableProcessors()); 
        //查看CPU几核几线程,线程数见任务管理器,核数见设备管理器 
    } 
}

  其中atomicReference的数据结果为:

  

  2个线程同时执行push(E item)操作时,一个线程在compareAndSet(oldHead, newHead)时将失败,将继续执行compareAndSet(oldHead, newHead)直至成功。

    

  下面的部分是抄的,还不是很理解,留着以后看吧。    

       前面说过了,CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。但CAS就没有开销了吗?不!有cache miss的情况。这个问题比较复杂,首先需要了解CPU的硬件体系结构:

   

  上图可以看到一个8核CPU计算机系统,每个CPU有cache(CPU内部的高速缓存,寄存器),管芯内还带有一个互联模块,使管芯内的两个核可以互相通信。在图中央的系统互联模块可以让四个管芯相互通信,并且将管芯与主存连接起来。数据以“缓存线”为单位在系统中传输,“缓存线”对应于内存中一个 2 的幂大小的字节块,大小通常为 32 到 256 字节之间。当 CPU 从内存中读取一个变量到它的寄存器中时,必须首先将包含了该变量的缓存线读取到 CPU 高速缓存。同样地,CPU 将寄存器中的一个值存储到内存时,不仅必须将包含了该值的缓存线读到 CPU 高速缓存,还必须确保没有其他 CPU 拥有该缓存线的拷贝。

  比如,如果 CPU0 在对一个变量执行“比较并交换”(CAS)操作,而该变量所在的缓存线在 CPU7 的高速缓存中,就会发生以下经过简化的事件序列:

  • CPU0 检查本地高速缓存,没有找到缓存线。
  • 请求被转发到 CPU0 和 CPU1 的互联模块,检查 CPU1 的本地高速缓存,没有找到缓存线。
  • 请求被转发到系统互联模块,检查其他三个管芯,得知缓存线被 CPU6和 CPU7 所在的管芯持有。
  • 请求被转发到 CPU6 和 CPU7 的互联模块,检查这两个 CPU 的高速缓存,在 CPU7 的高速缓存中找到缓存线。
  • CPU7 将缓存线发送给所属的互联模块,并且刷新自己高速缓存中的缓存线。
  • CPU6 和 CPU7 的互联模块将缓存线发送给系统互联模块。
  • 系统互联模块将缓存线发送给 CPU0 和 CPU1 的互联模块。
  • CPU0 和 CPU1 的互联模块将缓存线发送给 CPU0 的高速缓存。
  • CPU0 现在可以对高速缓存中的变量执行 CAS 操作了

  以上是刷新不同CPU缓存的开销。最好情况下的 CAS 操作消耗大概 40 纳秒,超过 60 个时钟周期。这里的“最好情况”是指对某一个变量执行 CAS 操作的 CPU 正好是最后一个操作该变量的CPU,所以对应的缓存线已经在 CPU 的高速缓存中了,类似地,最好情况下的锁操作(一个“round trip 对”包括获取锁和随后的释放锁)消耗超过 60 纳秒,超过 100 个时钟周期。这里的“最好情况”意味着用于表示锁的数据结构已经在获取和释放锁的 CPU 所属的高速缓存中了。锁操作比 CAS 操作更加耗时,是因深入理解并行编程。

  为锁操作的数据结构中需要两个原子操作。缓存未命中消耗大概 140 纳秒,超过 200 个时钟周期。需要在存储新值时查询变量的旧值的 CAS 操作,消耗大概 300 纳秒,超过 500 个时钟周期。想想这个,在执行一次 CAS 操作的时间里,CPU 可以执行 500 条普通指令。这表明了细粒度锁的局限性。


标签:java
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

发表评论
搜索
排行榜
KIKK导航

KIKK导航

关注我们