• 高并发编程之无锁


    前几期简单介绍了一些线程方面的基础知识,以及一些线程的一些基础用法以及通过jvm内存模型的方式去介绍了一些并发中常见的问题(想看往期文章的小伙伴可以直接拉到文章最下方飞速前往)。本文重点介绍一个概念“无锁”

    本期精彩
    什么是无锁
    无锁类的原理
    AtomicInteger
    Unsafe
    AtomicReference
    AtomicStampedReference

    什么是无锁

      在高并发编程中最重要的就是获取临界区资源,保证其中操作的原子性。一般来说使用synchronized关键字进行加锁,但是这种操作方式其实是将synchronized中的代码块由并行转为串行,虽然说这是一个解决并发问题的方法,但是这样的代码效率会显得比较低下。最比较高效的方法就是无锁,一般加锁的方法在多线程访问时,如果临界区资源被占用,系统就会将其他线程进行阻塞,挂起,但是无锁不会,它只会一次一次的重试,直到执行成功为止。在jdk中为我们提供了一系列的无锁类来供我们使用。

    无锁类的原理

    • CAS(Compare And Swap)比较并交换
      CAS算法:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。当且仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。而失败得线程不会被挂起,只是被通知失败,而线程则会再次尝试,也可以设置失败则不继续尝试访问。
    • CAS操作得CPU指令(cmpxchg)
      有些人有疑惑,在CAS操作中,步骤如此之多,会不会是非原子操作,如果是非原子操作会不会引起线程不安全的情况。其实CAS操作属于cpu指令cmpxchg完成的,通过指令操作保证为原子操作。

    AtomicInteger

      AtomicInteger为无锁整数,它其中的方法都是无锁的,它内部主要得接口有以下几个:

    方法名返回值参数描述
    get() int 获取当前值
    set() newValue 设置当前值
    getAndSet() int newValue 设置新值,返回旧值
    compareAndSet() boolean int expect, int u 如果内存中的值为expect,则设置新值为u,并且返回true
    getAndIncrement() int 当前值+1,返回旧值
    getAndDecrement() int 当前值-1,返回旧值
    getAndAdd() int delta 当前值增加delta,返回旧值
    incrementAndGet() int 当前值+1,返回新值
    decrementAndGet() int 当前值-1,返回新值
    addAndGet() int int delta 当前值增加delta,返回新值

      我们来看其中两个比较典型的方法的实现:

    • compareAndSet(int expect, int update):这个方法为如果内存中的值为expect,则设置新值为update,并且返回true,反之则设置失败,返回false
    /**
         * 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);
        }
     

      上述方法中,出现了几个参数valueOffset表示一个偏移量,expect表示一个预期值,update表示一个新的值,而调用得compareAndSwapInt方法则表示,在这个类的valueOffset的偏移量上得值是否与expect的值一致,如果一致,则将值修改为update的值,否则则设置失败。

    • getAndIncrement()当前值+1,返回旧值
    /**
     * Atomically increments by one the current value
     * 
     * @return the previous value
     */
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next)) {
                return current;
            }
        }
    }
     

      getAndIncrement()方法通过死循环的方式确保可以一致进行修改操作,但是一旦修改成功则跳出,否则一直修改。
      我们来看一个具体的例子:

    /**
     * @escription:无锁累加
     * @author: Herrt灬凌夜
     * @date: 2019年3月2日 下午10:21:53 
     */
    public class Tets1 {
    
        public AtomicInteger num = new AtomicInteger();
        public void accumulation () {
            for (int i = 0; i < 10000; i++) {
                num.incrementAndGet();
            }
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread [] ts = new Thread[10];
            final Tets1 test = new Tets1();
            for (int i = 0; i < ts.length; i++) {
                ts[i] = new Thread(new Runnable() {
    
                    public void run() {
                        test.accumulation();
                    }
                });
                ts[i].start();
            }
            for (Thread thread : ts) {
                thread.join();
            }
            System.out.println(test.num);
        }
    }
     

      在上述例子中我并没有对accumulation()方法进行加锁,但是最后得到的结果依旧是100000。所以可以说明这个操作是线程安全的。

    Unsafe

      Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。但是它是非公开的API,所以在不同得JDK版本中,差异比较大,但是它在JDK开发中应用非常多。
      Unsafe类通过偏移量这个概念使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。
      它内部主要的接口有以下几个:

    方法名返回值参数描述
    getInt() int Object o, long offset 获得给定对象偏移量上的int值
    putInt() void Object o, long offset, int x 设置给定对象偏移量上的int值
    objectFieldOffset() long Field f 获得字段在对象中的偏移量
    putIntVolatile() void Object o, long offset, int x 设置给定对象的int值,使用volatile语义
    getIntVolatile() int Object o, long offset 获得给定对象的int值,使用volatile语义
    putOrderedInt void Object o, long offset, int x 和putIntVolatile一样,但是它要求被操作得字段是volatile修饰的

      上述的几个方法都是被native关键字所修饰,因为Unsafe的实现是由C语言实现的。Java平台有个用户和本地C代码进行互操作的API,称为Java Native Interface (Java本地接口)。

    AtomicReference

      AtomicReference引用做了修改,是一个模版类,抽象了数据类型,如果说AtomicInteger修改的是一个整数,那么AtomicReference修改的就是一个对象。它其中的方法与AtomicInteger的方法大致一致,只是在类上加了一个范型。
      我们看下面实例:

    /**
     * @escription:AtomicReference实例
     * @author: Herrt灬凌夜
     * @date: 2019年3月3日 下午3:42:38 
     */
    public class AtomicReferenceTest {
    
        public AtomicReference atomicStr = new AtomicReference("修改前");
        public void accumulation () {
            if(atomicStr.compareAndSet("修改前", "修改后")) {
                System.out.println("Thread:" + Thread.currentThread().getId() + "修改成功!");
            } else {
                System.out.println("Thread:" + Thread.currentThread().getId() + "修改失败!");
            }
        }
    
        public static void main(String[] args) {
            final AtomicReferenceTest reference = new AtomicReferenceTest();
            for (int i = 0; i < 10; i++) {
                new Thread(new Runnable() {
    
                    public void run() {
                        reference.accumulation();
                    }
                }).start();
            }
        }
    }
     

      执行上面代码可以得出,只有一个线程修改成功,其他线程均修改失败,可以看出AtomicReference为线程安全的。

    AtomicStampedReference

      AtomicStampedReference也是用于修改一个对象的,但是这个类中加入了一个邮戳的标记,而这是为了解决ABA问题的,何为ABA问题呢,就是说一个线程将值修改为B,但是又被其他线程修改为A,这样其他线程又会继续去修改A.
      我们将AtomicReference中的实例做修改:

    public class AtomicReferenceTest {
    
        public AtomicReference atomicStr = new AtomicReference("修改前");
        public void accumulation () {
            if(atomicStr.compareAndSet("修改前", "修改后")) {
                System.out.println("Thread:" + Thread.currentThread().getId() + "修改成功!");
            } else {
                System.out.println("Thread:" + Thread.currentThread().getId() + "修改失败!");
                atomicStr.compareAndSet("修改后", "修改前");
            }
        }
    
        public static void main(String[] args) {
            final AtomicReferenceTest reference = new AtomicReferenceTest();
            for (int i = 0; i < 10; i++) {
                new Thread(new Runnable() {
    
                    public void run() {
                        reference.accumulation();
                    }
                }).start();
            }
        }
    }
     

      在我们预期之中,修改成功只能被执行一次,但是由于其他线程的原因,执行成功被执行多次。而AtomicStampedReferenve就是来解决这类问题的。
      我们来看一个例子,我们模拟用户消费,当用户首次余额不足20元时,系统赠送20元。

    /**
     * @escription:AtomicStampedReference
     * @author: Herrt灬凌夜
     * @date: 2019年3月3日 下午6:57:36 
     */
    public class AtomicStampedReferenceTest {
        AtomicStampedReference<Integer> money = new AtomicStampedReference<Integer>(19, 0);
    
        /**
         * 充值
         * @Title: recharge   
         * @Description: 当余额第一次不足20元时,系统充值20元
         * @param: @param timestamp      
         */
        public void recharge(int timestamp) {
            while (true) {
                while (true) {
                    Integer m = money.getReference();
                    if (m < 20) {
                        if (money.compareAndSet(m, m + 20, timestamp, timestamp + 1)) {
                            System.out.println("余额小于20,充值成功,当前余额为:" + money.getReference());
                            break;
                        } else {
                            break;
                        }
                    }
                }
            }
        }
    
        /**
         * 消费
         * @Title: consumption   
         * @return: void      
         */
        public void consumption() {
            for (int i = 0; i < 100; i++) {
                while (true) {
                    int timestamp = money.getStamp();
                    Integer m = money.getReference();
                    if (m > 10) {
                        if (money.compareAndSet(m, m - 10, timestamp, timestamp + 1)) {
                            System.out.println("消费10元,余额:" + money.getReference());
                            break;
                        }
                    } else {
                        System.out.println("余额不足!");
                        break;
                    }
                    break;
                }
            }
        }
    
        public static void main(String[] args) {
            final AtomicStampedReferenceTest test = new AtomicStampedReferenceTest();
            final int timestamp = test.money.getStamp();
            for (int i = 0; i < 3; i++) {
                new Thread(new Runnable() {
    
                    public void run() {
                        test.recharge(timestamp);
                    }
                }).start();
            }
    
            new Thread(new Runnable() {
    
                public void run() {
                    test.consumption();
                }
            }).start();
    
        }
    }

      执行结果发现,充值只发生1次,不会因为消费之后余额小于20元再次充值。
      我们去查看AtomicStampedReference类,发现其中存在一个内部类Pair:

    private static class Pair<T> {
            final T reference;
            final int stamp;
            private Pair(T reference, int stamp) {
                this.reference = reference;
                this.stamp = stamp;
            }
        }
     

      这个类中的Pair类代替了AtomicReference中的value,其中reference相当于value,而stamp则为一个标识。我们查看compareAndSet的源码:

    public boolean compareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));
        }

      我们发现,这里不仅仅去比较了reference的值,也去比较了stamp 的值,只有他们得值都相等,才会去执行cas操作。

  • 相关阅读:
    Halcon 笔记3 形态学
    Halcon 笔记2 Blob分析
    Halcon 笔记1
    线程
    Fn+F1-F12,避免使用FN+
    改变与接受
    PictureBox使用异常
    (一)Knockout
    (二)HTML5
    (一)chrome扩展
  • 原文地址:https://www.cnblogs.com/wuyx/p/10480349.html
Copyright © 2020-2023  润新知