一步步学习多线程(六) 无锁(CAS及无锁类)

无锁:无障碍的运行。

CAS:它包含3个参数CAS(V,E,N),V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N。如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS是乐观态度的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新。失败的线程不会被挂起,仅被告知失败,并允许再次尝试。当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理

无锁类:

1、AtomicInteger extends Number

主要接口:

    1)public final int get()     // 取得当前值

    2)public final void set(int newValue)      // 设置当前值

    3)public final int getAndSet(int newValue)    //设置新值,并返回旧值

    4)public final boolean compareAndSet(int expect,int u)    // 如果当前值为expect,则设置为u

    5)public final Int getAndIncrement()    //当前值加1,返回旧值

    6)public final Int getAndDecrement()    // 当前值减1,返回旧值

    7)public final int getAndAdd(int delta)    //当前值增加delta,返回旧值

    8)public final int incrementAndGet()    //当前值加1,返回新值

    9)public final int decrementAndGet()    //当前值减1,返回新值

    10)public final int addAndGet(int delta)    //当前值加delta,返回新值

我们选取两个方法来看一下源码compareAndSet() 和getAndIncrement() (JDK1.8)

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

    }

    比较expect和当前值,如果相同就将值改为update,返回true,如果不相同,就返回false,调用的是unsafe的方法,unsafe是Java底层的方法,用C写成,valueOffset表示一个偏移量。

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

   JDK1.8中,这个方法是直接调用的unsafe的方法,在之前的版本中,该方法的写法为:

   for(;;){

       int current = get();

       int next = current + 1;

       if(compareAndSet(current, next))

               return current;

   }

   这种写法比较明确,其实就是一个死循环,如果当前值与期望值不同,就继续循环,直到成功为止。

例子:

public class AtomicIntegerDemo {

       static AtomicInteger i = new AtomicInteger();

       public static class AddThread implements Runnable {

              public void run(){

                      for(int k=0;k<10000;k++){

                             i.increamentAndGet();

                       }

               }

      }

      public static void main(String[] args) {

               Thread[] ts = new Thread[10];

               for(int k=0;k<ts.length;k++) {

                       ts[k] = new Thread(new AddThread());

               }

               for(int k=0;k<ts.length;k++) {

                       ts[k].start();

               }

               for(int k=0;k<ts.length;k++) {

                       ts[k].join();

               }

               System.out.println(i);

     }

}

2、AtomicReference

AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。

例子:

public class AtomicReferenceDemo {
    
    public static AtomicReference<String> atomicStr = new AtomicReference<String>("hello");
    
    public static void main(String[] args) {
        
        for (int i = 0; i < 10; i++) {
            new Thread() {

                @Override
                public void run() {
                    System.out.println("Thread" + Thread.currentThread().getId() + ":start");
                    try {
                        Thread.sleep((long) Math.abs(Math.random() * 1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if(atomicStr.compareAndSet("hello", "olleh")){
                        System.out.println("Thread" + Thread.currentThread().getId() + ":change success");
                    }else{
                        System.out.println("Thread" + Thread.currentThread().getId() + ":change fail");
                    }
                }
                
            }.start();
        }
    }
}

但是这个类有缺陷,如果仅仅是加法,则问题不大,如果是和过程数据有关的,就出现问题了。比如线程一将变量a设置成A,线程二将变量a设置成了B,线程三又将变量a改成了A,如果这时线程四是想把变量a由A改成C的,由于之前经过变化a从A又变回了A,线程四会误认为变量a并未有变动,造成数据的误修改(ABA问题)。具体参加下文

https://www.cnblogs.com/love-jishu/p/5007882.html,为了解决这类问题,就引出了AtomicStampedReference

3、AtomicStampedReference

AtomicReference无法解决上述问题的根本是因为对象在修改过程中,丢失了状态信息。对象值本身与状态被画上了等号。因此,我们只要能够记录对象在修改过程中的状态值,就可以很好的解决对象被反复修改导致线程无法正确判断对象状态的问题。

看一下源码:

  1.     private static class Pair<T> {
  2.         final T reference;
  3.         final int stamp;
  4.         private Pair(T reference, int stamp) {
  5.             this.reference = reference;
  6.             this.stamp = stamp;
  7.         }
  8.         static <T> Pair<T> of(T reference, int stamp) {
  9.             return new Pair<T>(reference, stamp);
  10.         }
  11.     }
  12.     private volatile Pair<V> pair;

pair就是包装的value,它有一个reference和一个stamp(邮戳,可以理解为记录数据的更新次数)

of是一个静态的工厂方法,根据reference和stamp返回一个pair的实例;

  1.     public boolean compareAndSet(V   expectedReference,
  2.                                  V   newReference,
  3.                                  int expectedStamp,
  4.                                  int newStamp) {
  5.         Pair<V> current = pair;
  6.         return
  7.             expectedReference == current.reference &&
  8.             expectedStamp == current.stamp &&
  9.             ((newReference == current.reference &&
  10.               newStamp == current.stamp) ||
  11.              casPair(current, Pair.of(newReference, newStamp)));
  12.     }

当期望的值与当前值相同、期望的stamp与当前的stamp相同情况下,或者CAS操作成功就成功更新。

casPair(current, Pair.of(newReference, newStamp))的源码如下:

  1.     private boolean casPair(Pair<V> cmp, Pair<V> val) {
  2.         return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
  3.     }

cmp是比较的值,val是新的值

4、AtomicIntegerArray

针对于整数数组的支持,相关的api与AtomicInteger相比就是要增加一个数组下标。数组当中每一个元素都是线程安全的。

先看源码:

首先在类的内部维护一个普通的数组

    private final int[] array;

    static {
        int scale = unsafe.arrayIndexScale(int[].class);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }   

    public final int get(int i) { // 获取下标为i的值
        return getRaw(checkedByteOffset(i));
    }

    private int getRaw(long offset) {
        return unsafe.getIntVolatile(array, offset);   // 从数组的基地址开始,偏移量是offset的值
    }

    那么offset从哪里来的呢?继续看源码

    private long checkedByteOffset(int i) {
        if (i < 0 || i >= array.length)
            throw new IndexOutOfBoundsException("index " + i);

        return byteOffset(i);  // 数组中的便宜量是多少
    }

    private static long byteOffset(int i) {
        return ((long) i << shift) + base; 
    }

base:数组头的基地址,通过unsafe.arrayBaseOffset(int[].class)获得

scale:数组元素的大小,通过unsafe.arrayIndexScale(int[].class),由于是int,这个大小是4,如果要访问的是第T个元素的

那么地址就是base + T*scale

shift的计算方法:shift = 31 - Integer.numberOfLeadingZeros(scale),numberOfLeadingZeros指的是二进制数字的前导零,int scale = 4; Integer.numberOfLeadingZeros(scale); 返回 scale 高位连续0的个数,得出shift = 2

shift就是用来定位数组中的内存位置,用来移位用的,每向左移动移位,在不越界的情况下,想当于乘以2。也就是int类型的长度为4,也就是第0个位置是0,第1(i)个位置是4,,第二个(i)位置是8,也就是偏移位置等于  i * 4,也就是  i << 2;

例子:

  1. public class AtomicIntegerArrayDemo {
  2.     
  3.     public static AtomicIntegerArray arr = new AtomicIntegerArray(10);
  4.     
  5.     public static class AddThread extends Thread {
  6.         @Override
  7.         public void run() {
  8.             for (int i = 0; i < 100000; i++) {
  9.                 // 对arr的第i的元素加1
  10.                 arr.getAndIncrement(i%arr.length());
  11.             }
  12.         }
  13.     }
  14.     public static void main(String[] args) throws InterruptedException {
  15.         Thread[] ts = new Thread[arr.length()];
  16.         
  17.         for (int i = 0; i < ts.length; i++) {
  18.             ts[i] = new AddThread();
  19.         }
  20.                 
  21.         for (int j = 0; j < ts.length; j++) {
  22.             ts[j].start();
  23.         }
  24.         
  25.         for (int k = 0; k < ts.length; k++) {
  26.             ts[k].join();
  27.         }
  28.         System.out.println(arr);
  29.     }
  30. }

5、AtomicIntegerFieldUpdate

将普通变量享受原子操作。主要方法newUpdate() 及 incrementAndGet()

例子

public class AtomicIntegerFieldUpdateDemo {
    
    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 {
        Candidate stu = new Candidate();
        Thread[] ts = new Thread[10000];
        for (int i = 0; i < ts.length; i++) {
            ts[i] = new Thread() {
                
                @Override
                public void run() {
                    if(Math.random() > 0.4){
                        scoreUpdater.incrementAndGet(stu);
                        allScore.incrementAndGet();
                    }
                }
            };
            ts[i].start();
        }
        for (int i = 0; i < ts.length; i++) {
            ts[i].join();
        }
        
        System.out.println(stu.score);
        System.out.println(allScore);
    }
}

猜你喜欢

转载自blog.csdn.net/money9sun/article/details/81014302