zoukankan      html  css  js  c++  java
  • CAS(比较并交换)

     一、CAS(无锁的执行者)

      CAS包含3个参数:内存值  V  旧的预期值  A  新值  B

      当且仅当V值等于A值时,将V的值改为B值,如果V值和A值不同,说明已经有其他线程做了更新,则当前线程什么都不做,最后返回当前V的真实值。CAS操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功地完成操作。

      当多个线程同时使用CAS同时操作同一个变量时,只有其中一个线程会胜出并成功更新,其余均会失败;但失败的线程并不会挂起,仅是被告知失败,并且允许再次尝试,也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作的影响,并执行相应的处理措施。CAS的关键点在于,系统在硬件层面保证了比较并交换操作的原子性,处理器使用基于对缓存加锁或总线枷锁的方式来实现多处理器之间的原子操作。

      由于是无锁操作,因此不可能出现死锁情况。CAS是非阻塞算法的一种常见实现。CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

      一个线程间共享的变量,首先在主存中会保留一份,然后每个线程的工作内存也会保留一份副本。这里说的预期值,就是线程保留的副本。当该线程从主存中获取该变量的值后,主存中该变量可能已经被其他线程刷新了,但是该线程工作内存中该变量却还是原来的值,这就是所谓的预期值了。当你要用CAS刷新该值的时候,如果发现线程工作内存和主存中不一致了,就会失败,如果一致,就可以更新成功。

    CAS的优缺点

      CAS由于是在硬件层面保证的原子性,不会锁住当前线程,它的效率是很高的。

      CAS虽然很高效地实现了原子操作,但它依然存在三个问题。如下:

    ABA问题

      CAS会导致“ABA问题”。CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差中可能导致数据发生变化。

      比如一个线程one从内存位置V中取出A,这是另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

      部分乐观锁的实现是通过版本号(version)的方式来解决ABA问题,乐观锁每次在执行数据的修改操作时,都会带上一个版本号, 一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

    循环时间长开销大

      自选CAS如果长时间不成功,会给CPU带来非常大的执行开销。因此CAS不适合竞争十分频繁的场景。

    只能保证一个共享变量的原子操作

      当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

    这里粘贴一个,模拟CAS实现的计数器:

    public class CASCount implements Runnable{
        private SimilatedCAS counter = new SimilatedCAS();
    
        public void run(){
            for(int i=0;i<10000;i++){
                System.out.println(this.increment());
            }
        }
    
        public int increment(){
            int oldValue = counter.getValue();
            int newValue = oldValue + 1;
    
            while(!counter.compareAndSwap(oldValue,newValue)){
                //如果CAS失败,就去拿新值继续执行CAS
                oldValue = counter.getValue();
                newValue = oldValue + 1;
           }
            return newValue;
        }
    
        public static void main(String[] args){
            Runnable run = new CASCount();
    
            new Thread(run).start();
            new Thread(run).start();
            new Thread(run).start();
            new Thread(run).start();
            new Thread(run).start();
        }
    }
    
    class SimilatedCAS{
        private int value;
    
        public int getValue(){
            return value;
        }
    
        //这里只能用synchronized了,毕竟无法调用操作系统的CAS
        public synchronized boolean compareAndSwap(int expectedValue,int newValue){
            if(value == expectedValue){
                value = newValue;
                return true;
            }
            return false;
        }
    }

    二、原子包  java.util.concurrent.atomic

      JDK1.5的原子包:java.util.concurrent.atomic这个包里面提供了一组原子类。其基本特性是:在多线程环境下,当有多个线程同时执行这些类实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择另一个线程进入。相对于synchronized这种阻塞算法,Atomic类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关的指令来保证的,由于一般CPU切换时间比CPU指令集操作更加长,所以JUC在性能上有了很大的提升。

      AtomicInteger是一个支持原子操作的Integer类,就是保证对AtomicInteger类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致的问题。如果不使用AtomicInteger,要实现一个按顺序获取的ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的ID的现象。Java并发库中的AtomicXXX类均是基于这个原语的实现,拿出AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的:

      来看看++i是怎么做到的。如下代码:

     1 public class AtomicInteger extends Number implements java.io.Serializable { 
     2 private volatile int value; 
     3 public final int get() { 
     4          return value; 
     5          } 
     6 public final int getAndIncrement() { 
     7          for (;;) { //CAS 自旋,一直尝试,直达成功
     8              int current = get(); 
     9              int next = current + 1; 
    10              if (compareAndSet(current, next)) 
    11                return current; 
    12          } 
    13 } 
    14 public final boolean compareAndSet(int expect, int update) { 
    15          return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
    16 } 
    17 }

      在这里getAndIncrement采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。而compareAndSet利用JNI来完成CPU指令的操作,非阻塞算法。

      其中,unsafe.compareAndSwapInt()是一个native方法,正是调用CAS原语完成该操作。

    首先假设有一个变量i,i的初始值为0。每个线程都对i进行+1操作。CAS是这样保持同步的:

      假设有两个线程,线程1读取内存中的值为0,current=0,next=1,然后挂起,然后线程2对i进行操作,将i的值变成了1。线程2执行完,回到线程1,进入if里的compareAndSet方法,该方法进行的操作的逻辑是,(1)如果操作数的值在内存中没有被修改,返回true,然后compareAndSet方法返回next的值;(2)如果操作数的值在内存中被修改了,则返回false,重新进入下一次循环,重新得到current的值为1,next的值为2,然后再比较,由于这次没有被修改,所以直接返回2。

      那么为什么自增操作要通过CAS来完成?仔细观察getAndIncrement()方法,发现自增操作其实拆成了两步完成的:

        int current = get();

        int next = current +1;

      由于volatile只能保证读取或写入的是最新值,那么可能出现以下情况:

        1)A线程执行get()操作,获取current值(假设为1)

        2)B线程执行get()操作,获取current值(为1)

        3)B线程执行next = current +1操作,next=2

        4)A线程执行next = current +1操作,next=2

      这样的结果明显不是我们想要的,所以,自增操作必须采用CAS来完成。

      

  • 相关阅读:
    docker镜像
    docker常用命令
    docker基础
    跨站脚本漏洞(XSS)基础
    Session、Cookie与Token
    linux之curl工具
    ssl证书与java keytool工具
    mysql主从复制
    linux之平均负载(学习笔记非原创)
    mysql8.0忘记密码如何操作?
  • 原文地址:https://www.cnblogs.com/HuiH/p/11905021.html
Copyright © 2011-2022 走看看