• 多线程(9) — 无锁


      对于并发控制而言,锁是一种悲观策略,它总是假设每一次的临界区操作会产生冲突。如果有多个线程同时需要访问临界区资源,则宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。而无锁是一种乐观策略,它会假设对资源的访问是没有冲突的,既然没有冲突就不会让线程等待,所有线程可以在不停顿的情况下持续执行。那遇到冲突了咋办呢?无锁的策略使用一种比较交换的技术(CAS,Compare And Swap)来鉴别线程冲突,一旦检测到线程冲突,就重试当前操作到没有冲突为止。

    1. 比较交换(CAS)

      与锁相比,使用比较交换会使程序看起来复杂一些,但是由于非阻塞性,对死锁问题天生免疫,并且线程间的相互影响也远远比基于锁的方式小。更为重要的是,使用无锁方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此它要基于锁的方式拥有更优越的性能。

      CAS算法过程是:它包含三个参数CAS(V,E,N),V 表示要更新的变量,E 表示预期值,N 表示新值。仅当 V 值等于 E 值时,才会将 V 的值设为N,如果 V 值和 E 值 N,如果 V 和 E 不同,说明其他线程做了更新,则当前线程什么都不做。最后CAS返回当前 V 的真实值。CAS 操作是抱着乐观态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,均会失败。失败的线程不会被挂起,仅是告知失败,并且允许再次尝试,也允许失败的线程放弃操作。

      简单的说CAS 需要额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,则说明它已经被别人修改过了,就要重新读取,再次尝试修改就OK了。在硬件层面,大部分现代处理器都已经支持原子化的CAS指令。

    2. 无锁的线程安全整数:AtomicInteger

      JDK中有个atomic包,里面实现了一些直接使用CAS操作的线程安全的类型。其中,最常用的一个类就是AtomicInteger,可以看作一个整数。与Integer不同的是,它可变,且线程安全。对其修改等任何操作使用CAS指令进行的。

    public final int get()                                           //取得当前值
    public final void set(int newValue)                              //设置当前值
    public final int getAndSet(int newValue)                         //设置新值,并返回旧值
    public final boolean compareAndSet(int expect, int update)       //如果当前值为expect,则设置为update
    public final int getAndIncrement()                               //当前值加1,返回旧值
    public final int getAndDecrement()                               //当前值减1,返回旧值
    public final int getAndAdd(int delta)                            //当前值增加delta,返回旧值
    public final int incrementAndGet()                               //当前值加1,返回新值
    public final int decrementAndGet()                               //当前值减1,返回新值
    public final int addAndGet(int delta)                            //当前值增加delta,返回新值

      就内部实现来说,AtomicInteger中保存了一个核心字段:

    private volatile int value;

      它代表了AtomicInteger的当前实际取值,此外还有一个变量,保存这value字段在AtomicInteger中的偏移量:

    private static final long valueOffset;

      下面给出一个使用AtomicInteger的简单例子:

    public class AtomicIntegerDemo {
    
        static AtomicInteger i = new AtomicInteger();
        public static class AddThread implements Runnable{
            @Override
            public void run() {
                for (int k = 0; k < 10000; k++) {
                    i.incrementAndGet();
                }
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            Thread[] ts = new Thread[10];
            for (int m = 0; m < 10; m++) {
                ts[m] = new Thread(new AddThread());
            }
            for (int m = 0; m < 10; m++) {
                ts[m].start();
            }
            for (int m = 0; m < 10; m++) {
                ts[m].join();
            }
            System.out.println(i);
        }
    }

      上述例子中,incrementAndGet()方法会使用CAS操作将自己的值加1,同时返回当前值。执行这段代码,输出10000,说明程序执行正常,没有错误。如果线程不安全,输出的肯定是小于10000的。基于JDK1.7 可以看到incrementAndGet()方法的内部实现如下:

    public final int incrementAndGet() {
        for (;;) {
           int current = get();
           int next = current + 1;
           if (compareAndSet(current, next))
              return next;
       }
    }

      CAS操作未必是成功的,因此对于不成功的情况,我们就需要不断进行尝试。get()取得当前值,接着加1后得到新值next,这样我们就得到了CAS必须的俩参数:期望值和新值。使用compareAndSet()方法将新值next写入,成功的条件是在写入时刻,当前值应该要等于刚刚取得current。如果不是这样,则说明AtomicInteger的值被其他线程修改过了。当前线程看到的状态是一个过期状态,compareAndSet返回失败,需要进行下一次重试,直到成功。

      和AtomicInteger类似的类还有:AtomicLong用来代表long类型数据;AtomicBoolean表示boolean型数据;AtomicReference表示对象引用。

    3. Java中的指针:Unsafe类

      compareAndSet()方法的实现就使用了Unsafe:

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

      指针是不安全的,因此Java把指针去除,如果指针指错了位置,结果是灾难性的,可能导致系统的崩溃。Unsafe类封装了类似指针的操作。compareAndSwapInt()方法是一个navtive方法,它的几个参数含义如下:

    public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

      第一个参数o为给定的对象,offset为对象内的偏移量(其实就是一个字段到对象头部的偏移量,通过这个偏移量可以快速定位字段),expected表示期望值,x表示要设置的值。如果指定的字段等于expected,那么就把它设置为x。compareAndSwapInt()方法内部是使用CAS原子指令来完成的。此外Unsafe类还提供了一些方法,主要有以下几种:

    //获得给定对象偏移量上的int值
    public native int getInt(Object o,long offset);
    
    //设置给定对象偏移量上的int值
    public native void putInt(Object o, long offset, int x);
    
    //获得字段在对象中的偏移量
    public native long objectFieldOffset(Field f);
    
    //设置给定对象的int值,使用volatile语义
    public native void putIntVolatile(Object o, long offset, int x);
    
    //获得给定对象的int值,使用volatile语义
    public native int getIntVolatile(Object o, long offset);
    
    //和putIntVolatile()一样,但是它要求被操作字段就是volatile类型的
    public native void putOrderedInt(Object o, long offset, int x);

      Java虽然放弃了指针,但是关键时刻,类似指针的技术还是必不可少的。底层Unsafe类实现就是例子。但是获得Unsafe类实例的方法是调动工厂方法getUnsafe(),但是它的实现如下:

    public static Unsafe getUnsafe(){
        Class localClass = Reflection.getCallerClass();
        if (localClass.getClassLoader() != null)
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }

      从代码中可以看出,它会检查代用getUnsafe()函数的类,如果ClassLoader不是null就直接抛异常而拒绝工作。因此我们无法直接使用Unsafe这个类,它是JDK内部专属的类。

    注意:根据Java类加载器的工作原理,引用程序的类由App Loader加载。而系统核心类,例如rt.jar中的类由Bootstrap类加载器加载。这个加载器没有Java对象的对象,因此试图获得这个类加载器返回null。因此当一个类的加载器为null时,说明它是由Bootstrap类加载器加载的,而这个类也极有可能是rt.jar中的类。

    4.无锁对象的引用:AtomicReference

      AtomicReference是对应普通对象引用,可以保证在修改对象引用时的线程安全性。但是原子操作在逻辑上有一些不足。前面说过线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望值是否一致。假如在修改新值前,对象的值被其他线程连续修改两次,而经过两次修改以后对象值恢复为旧值,这样就无法判断这个对象究竟是否被修改过。如果在现实中是否能修改对象的值,不仅取决于当前值,还和对象的过程有关的话,这个类就无能为力了。举个例子:

      有个商店,为增加客户粘性,为VIP卡里余额小于20元的客户一次性赠送20元,但每个人只能赠送一次。现在模拟这个场景演示AtomicReference。

    static AtomicReference<Integer> money = new AtomicReference<Integer>();
    
    public static void main(String[] args) {
        money.set(18);
        for(int i=0;i<3;i++){
         // 模拟多个线程同时更新后台数据,为用户充值
    new Thread(){ @Override public void run() { while(true){ while(true){ Integer m = money.get(); if(m<20){ if(money.compareAndSet(m, m+20)){ System.out.println("余额小于20元,充值成功,余额:"+money.get()+"元");
                        break; } }
    else{ System.out.println("余额大于20元,无需充值!"); break; } } } } }.start(); } }

      上面的代码就判断了账户金额是不是大于20元,大于的再进行充值。但是在客户充值以后进行了一次消费,使得总金额又再次小于20元了,那么上述代码就会再次给账户充值,存在多次充值的可能。

    for(int i=0;i<3;i++){
        new Thread(){
            @Override
            public void run() {
                while(true){
                    Integer m = money.get();
                    if(m>=10){
                        System.out.println("大于10元!");
                        if(money.compareAndSet(m, m-10)){
                            System.out.println("成功消费10元,余额:"+money.get());
                            break;
                        }
                    }else{
                        System.out.println("没有足够的金额消费");
                        break;
                    }
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

      以上代码执行后,账户会反复的进行充值和消费,为了解决这样的问题,JDK提供了一个类AtomicStampedReference可以解决这个问题。

    5. 带有时间戳的对象引用:AtomicStampedReference

      AtomicReference无法解决上述问题的原因是,对象在修改过程中丢失了状态信息,对象值本身与状态被画上等号。AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还更新了时间戳。也就是对象值及时间戳都必须满足期望值,写入才会成功。即使是对象反复读写,写回原值,只要时间戳发生变化,就能防止不恰当写入。

    // 参数依次是期望值、写入新值、期望时间戳、新时间戳
    public boolean compareAndSet(V   expectedReference,V   newReference,int expectedStamp, int newStamp)
    
    // 设置当前对象引用和时间戳
    public void set(V newReference,int newStamp)

      上面AtomicReference的例子改成AtomicStampedReference后就不存在这样的事情了。

    6. 数组也能无锁:AtomicIntegerArray

      除提供基本数据类型外,JDK还为我们准备了数组复合结构,AtomicLongArray和AtomicReferenceArray类似。AtomicIntegerArray本质上是对 int[] 类型的封装,使用Unsafe类通过CAS的方式控制 int[] 在多线程下的安全性。它提供以下核心 API:

    //获得数组第i个下标的元素
    public final int get(int i)
    
    //获得数组的长度
    public final int length()
    
    //将数组第i个小标设置为newValue,并返回旧值
    public final int getAndSet(int i, int newValue)
    
    //进行CAS操作,如果第i个下标元素等于expect,设置为update,设置成功返回true
    public final boolean compareAndSet(int i, int expect, int update)
    
    //将第i个下标的元素加1
    public final int getAndIncrement(int i)
    
    //将第i个下标的元素减1
    public final int getAndDecrement(int i)
    
    //将第i个下标的元素增加delta
    public final int getAndAdd(int i, int delta)

      下面给出一个简单的例子,展示AtomicIntegerArray的使用方法:

    public class AtomicIntegerArrayDemo {
    
        static AtomicIntegerArray arr = new AtomicIntegerArray(10);
        public static class AddThread implements Runnable{
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    arr.getAndIncrement(i % 10);
                }
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            Thread[] ts = new Thread[10];
            for (int i = 0; i < 10; i++) {
                ts[i] = new Thread(new AddThread());
            }
            for (int i = 0; i < 10; i++) {
                ts[i].start();
            }
            for (int i = 0; i < 10; i++) {
                ts[i].join();
            }
            System.out.println(arr);
        }
    }

    7. 普通变量也可以的原子操作:AtomicIntegerFieldUpdater

      由于初期考虑不周,或者后期需求变化,一些普通变量可能也会有线程安全需求,如果改动不大,可以简单地修改程序中每一个使用或者读取这个变量的地方,显然这已经违背了开闭原则。现在可以使用这个Updater在不改变源代码的基础上让普通的变量也可以进行CAS操作带来的线程安全性。

      根据数据类型不同,Updater有三种,分别是AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicIntegerFieldUpdater,分别对int、long和对象进行CAS修改。

      假设某地要进行一次选举,模拟一个投票场景,如果选民投了候选人一票,记为1,否则记为0,最终的选票显然是所有数据的简单求和。

    package com.wyw.xc.test;
    
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
    
    public class AtomicIntegerFieldUpdaterDemo {
    
        public static class Candidate{
            int id;
            volatile int score;
        }
        public final static AtomicIntegerFieldUpdater<Candidate> scoreUpdater = 
                AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
        public static AtomicInteger allScore = new AtomicInteger(0);
        public static void main(String[] args) throws InterruptedException{
            final Candidate stu = new Candidate();
            Thread[] t = new Thread[10000];
            for (int i = 0; i < t.length; i++) {
                t[i]= new Thread(){
                    @Override
                    public void run() {
                        if(Math.random()>0.4){
                            scoreUpdater.incrementAndGet(stu);
                            allScore.incrementAndGet();
                        }
                    }
                };
                t[i].start();
            }
            for (int i = 0; i < t.length; i++) {
                t[i].join();
            }
            System.out.println("score="+stu.score);
            System.out.println("allScore="+allScore);
        }
    }

      上述代码模拟这个计票场景,候选人得票数量记录在Candidate.score中,这是个普通的volatile变量,而volatile变量不是线程安全的。allScore用来检查AtomicIntegerFieldUpdater的正确性,如果线程安全的话,score和allScore的值是相等的。运行结果这两者总是相等的,说明AtomicIntegerFieldUpdater保证了score的线程安全。但是使用AtomicIntegerFieldUpdater得注意以下事项:

    • Updater只能修改它可见范围内的变量,因为Updater使用反射得到这个变量。如果变量不可见,就会出错。比如把score声明为private,就是不可行的。
    • 为了确保变量被正确的读取,它必须是volatile类型的,如果我们原有代码中未声明这个类型,那么简单声明一下就行,这不会引起什么问题。
    • 由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此,它不支持static字段(Unsafe.objectFieldOffset()方法不支持静态变量)。

    8. 线程间互相帮助:细看SynchronousQueue的实现

      这个队列的容量是0,任何一个对这个队列的写需要等待一个对这个队列的读,反之亦然。与其说是一个队列,不如说是一个数据交换通道。put()和take()两种不同的方法抽象为一个共同的方法Transferer.transger(),字面上看,就是数据传递的意思  

    Object transfer(Object e,boolean timed,long nanos)

      当参数e为非空时,表示当前操作传递给一个消费者,如果为空时,表示当前操作需要请求一个数据。timed参数决定是否存在timeout时间,nanos决定了timeout时长,如果返回值非空,表示数据已经接受或者正常提供;如果返回值为空表示失败。

      SynchronousQueue内部维护一个线程等待队列。等待队列中会保存等待线程及相关数据的信息。比如,生产者将数据放入SynchronousQueue时,如果没有消费者接收,那么数据本身和线程对象都会打包在队列中等待。Transferer.transger()函数的实现是SynchronousQueue的核心,它大体上分为三个步骤:

    • 如果等待队列为空,或者队列中节点的类型和本次操作是一致的,那么将当前操作压入队列等待。比如,等待队列中是读线程等待,本次操作也是读,因此这两个读都需要等待。进入等待队列的线程可能会被挂起,它们等待一个匹配操作。
    • 如果等待队列的元素和本次操作是互补的(比如等待队列是读,而本次操作是写),那么就插入一个完成状态的节点,并且让它匹配到一个等待节点上。接着弹出这个节点,并且使得对应的这两个线程继续执行。
    • 如果线程发现等待队列的节点就是完成节点,那么帮助这个节点完成任务,其流程和步骤2一致的。

    步骤1执行如下代码:

    Object transfer(Object e, boolean timed, long nanos) {
    SNode s = null; // constructed/reused as needed
    int mode = (e == null) ? REQUEST : DATA;
    for (;;) {
         SNode h = head;
         if (h == null || h.mode == mode) {  // 队列为空或者模式相同
                if (timed && nanos <= 0) {      // 不等待
                   if (h != null && h.isCancelled())
                          casHead(h, h.next);     // 处理取消节点
                   else
                          return null;
                   } else if (casHead(h, s = snode(s, e, h, mode))) {
                        SNode m = awaitFulfill(s, timed, nanos); // 等待,直到有匹配操作出现
                        if (m == s) {               // 等待被取消
                             clean(s);
                             return null;
                        }
                        if ((h = head) != null && h.next == s)
                            casHead(h, s.next);     // 帮助s的 fulfiller
                        return (mode == REQUEST) ? m.item : s.item;
                  }
           }

      SNode表示等待队列中的节点,内部封装了当前线程、next节点、匹配节点、数据内容等信息。h==null行判断当前等待队列为空,或者队列元素的模式与本次操作相同。else if (casHead(h, s = snode(s, e, h, mode)))行生成一个新的节点并置于队列头部,这个节点就代表当前线程。

    步骤2的执行如下代码:

    else if (!isFulfilling(h.mode)) { // 是否处于fulfill状态
         if (h.isCancelled())            // 准备取消
             casHead(h, h.next);         // 弹出重试
             else if (casHead(h, s=snode(s, e, h,         FULFILLING|mode))) {
                for (;;) { // 一直循环知道匹配或者没有等待者了
                   SNode m = s.next;       // m是s的匹配者
                   if (m == null) {        // 已经没有等待者了
                       casHead(s, null);   // 弹出fulfill 节点
                       s = null;           // 下次用新的节点
                       break;              // 重新开始主循环
                   }
                   SNode mn = m.next;
                   if (m.tryMatch(s)) {
                       casHead(s, mn);     // 弹出 s 和 m
                          return (mode == REQUEST) ? m.item : s.item;
                   } else                  // match匹配失败
                   s.casNext(m, mn);   // 帮助删除节点
                }
          }
    }

    步骤3的实现,如果线程在执行时,发现头部元素恰好是fulfil模式,它就会帮助fulfill节点尽快被执行。

    else {                            // 帮助一个 fulfiller
      SNode m = h.next;               // m 是 h 的匹配
      if (m == null)                  // 没有等待者
          casHead(h, null);           // 弹出 fulfilling 节点
      else {
          SNode mn = m.next;
          if (m.tryMatch(h))          // 帮助 match
              casHead(h, mn);         // 弹出 h 和 m
          else                        // match失败
              h.casNext(m, mn);       // 帮助删除节点
      }
    }
  • 相关阅读:
    P3396 哈希冲突 分块
    大数据之路week01--自学之面向对象java(static,this指针(初稿))
    大数据之路week01--自学之集合_2(列表迭代器 ListIterator)
    大数据之路week01--自学之集合_2(List)
    大数据之路week01--自学之集合_2(Iterator迭代器)
    大数据之路week01--自学之集合_1(Collection)
    大数据之路day05_1--初识类、对象
    大数据之路day04_2--经典bug(equals与==比较不同,break的跳出不同)
    大数据之路day04_1--数组 and for循环进阶
    eclipse断点的使用---for循环举例
  • 原文地址:https://www.cnblogs.com/wangyongwen/p/11330109.html
Copyright © 2020-2023  润新知