高并发编程之无锁

时间:2021-06-02 17:39:49

前几期简单介绍了一些线程方面的基础知识,以及一些线程的一些基础用法以及通过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操作。

 

高并发编程之无锁