• 并发之CAS无锁技术


        CAS算法即是:Compare And Swap,比较并且替换;
        CAS算法存在着三个参数,内存值V,旧的预期值A,以及要更新的值B。当且仅当内存值V和预期值B相等的时候,才会将内存值修改为B,否则什么也不做,直接返回false;
        比如说某一个线程要修改某个字段的值,当这个值初始化的时候会在内存中完成,根据Java内存模型,该线程保存着这个变量的一个副本;当且仅当这个变量的副本和内存的值如果相同,那么就可以完成对值得修改,并且这个CAS操作完全是原子性的操作,也就是说此时这个操作不可能被中断。
        先来看一个n++的问题:
    public class Case {
        public volatile int n;
        
        public void add() {
            n++;
        }
    }

    上述代码中什么变量被volatile修饰,此时说明该变量在多线程操作的情况下可以保证内存的可见性,但是不可以保证原子性操作,因此在多线程并发的时候还是会出现问题的;利用Javap命令来看看汇编指令:

    PS D:ssh> javac Case.java
    PS D:ssh> javap -c Case
    Compiled from "Case.java"
    public class Case {
      public volatile int n;
    
      public Case();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public void add();
        Code:
           0: aload_0
           1: dup
           2: getfield      #2                  // Field n:I
           5: iconst_1
           6: iadd
           7: putfield      #2                  // Field n:I
          10: return
    }
    PS D:ssh>
    

      

    在方法add()中,第17行表示获取到了n的初始值;
                              第19行执行了iadd()操作,n加一;
                              第20行执行了putfield,把新累加的值赋值给n;
    在上面我很清楚的说过volatile确实无法保证上述三个操作步骤的原子性;可以使用synchrnoized的方法完成原子性的操作;synchrnoized是互斥锁,也是可重入的锁,可以保证操作的原子性;但是加锁之后效率降低,
        好了,接下来再看一段代码:
    public int a = 1;
    public boolean compareAndSwapInt(int b) {
        if (a == 1) {
            a = b;
            return true;
        }
        return false;
    }
    
    上述方法在并发的情况下也是会出现问题的;当多个线程直接进入compareAndSwapInt()之后,他们也同时符合上述的逻辑判断,此时对a的赋值也有可能同事发生,这样也带来了线程安全的问题;
    同样加锁的方式也可以解决这个问题,但是在这里我们不研究锁的问题;下面我们来看看一段代码,这是AtomicInteger类中的一部分源码:
    public class AtomicInteger extends Number implements java.io.Serializable {
        private static final long serialVersionUID = 6214790243416807050L;
    
        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private static final long valueOffset;
    
        static {
            try {
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField("value"));
            } catch (Exception ex) { throw new Error(ex); }
        }
    
        private volatile int value;
    
    
        /**
         * Gets the current value.
         *
         * @return the current value
         */
        public final int get() {
            return value;
        }
    }

     

    1 Unasfe是CAS的核心类,通过这个类可以获取字段在内存中的地址偏移量;Unsafe是native的,我们一般不可能使用;这是Java对硬件操作的支持;
    2 valueOffset是地址偏移量(变量在内存中的地址偏移量)
    3 value是使用volatile修饰的,保证了内存的可见性;
        平时做常用的方法addAndGet()方法;作用是原子性的操作给变量添加值;
    int addAndGet(int delta)           以原子方式将给定值与当前值相加。

    在Java8中,这个方法的实现是调用了unsafe()方法;因此我们看不到;

     public final int addAndGet(int delta) {
            return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
     }

    但是通过网上看到了该方法的实现方式:

    public final int addAndGet(int delta) {
        for (;;) {
            int current = get();
            int next = current + delta;
            if (compareAndSet(current, next))
                return next;
        }
    }
      public final int get() {
            return value;
        }
    假设delta的值为1,在CAS算法下操作的话,首先进入一个for循环体;假设存在着两个线程,并且内存中的值value=3;根据Java内存模型,每一个线程都存在这这个变量的副本;
        1) 线程1进入循环体,获取到current的值为3,然后获取到到next的值此时为4;此时假设线程1运气不好,被挂起;
        2)线程2进入循环体,获取到current的值为3,同时next的值也为4;线程2运气好,此时继续执行compareAndSet()方法;
    public final boolean compareAndSet(int expect, int update) {
            return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
     }
        线程2传入两个参数,一个当前值,以及一个预期值;当前值,也就是current=3.要修改成为4;此时当前值也就是预期值和内存中的value比较,此时都是3,那么修改内存中的值为4;
        3)线程1此时再次执行compareAndSwapInt()方法的时候。发现内存中的值为4,预期的值是3,两者不相等,此时就不可以再次赋值了;
     
    CAS的缺点:
        CAS存在和“ABA的漏洞”;什么是ABA呢?
        假定在某个时刻某个线程从内存中取出A,然后在下个时刻准备更新这个值;在这个时间差内数据发生了改变;
     假设线程1从内存中取出了A,线程2也从内存中取出了A,并且将值修改为B,最后又改为A,当线程1去更新值得时候发现内存中的数据和线程备份数据相同,可以更新;但是此时内存中的值其实发生了变化的,只不过又变回去了;在实际的开发过程中,ABA可能会带来一些问题,但是我认为无关紧要,我们需要的只是数值的变化而已;
        对于单向链表实现的栈而言;假设存在一个链表  A---->B;线程1要去将栈顶的数据修改为B,但是此时线程2进来之后,A---->B出栈,D、C、A压栈;此时链表的结构发生了变化;A---->C---->D;此时线程1发现栈顶元素还是A,而元素B被出栈之后成为一个游离的对象,
        解决方式:由于CAS算法没有直接的使用锁;而是通过乐观锁的方式去控制并发的;而对于乐观锁而言一般都是操作+时间戳来控制每一次的版本号的;在JDK类库中,可以使用AutomicStampReference来解决
  • 相关阅读:
    (转) hive调优(2)
    (转)hive调优(1) coding调优
    hive on tez 错误记录
    tez 0.9.0 配置
    hive on tez
    让博客园自动生成目录
    hive --metastore三种模式
    hive 使用beelin连接报错
    mysql my.cnf文件
    1、Kfaka 部署
  • 原文地址:https://www.cnblogs.com/gosaint/p/9045494.html
Copyright © 2020-2023  润新知