Java多线程(6)CAS详解

上一篇:Java多线程(5)JMM详解

CAS

一. CAS 是什么

CAS是CompareAndSwap(也可以是CompareAndSet)的简称,从字面上理解就是比较并更新,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止,看下面代码

这个代码在多线程下是线程安全的,AtomicInteger内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?

CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果,如上面代码案例中的AtomicInteger这个对象,他就是通过volatile实现在没有锁的情况下做到数据正确性的 private volatile int value

二 .CASapi

1. 原子整数

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

以 AtomicInteger 为例

     AtomicInteger i = new AtomicInteger(0);
        // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
        System.out.println(i.getAndIncrement());
        // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
        System.out.println(i.incrementAndGet());
        // 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
        System.out.println(i.decrementAndGet());
        // 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
        System.out.println(i.getAndDecrement());
            // 获取并加值(i = 0, 结果 i = 5, 返回 0)
        System.out.println(i.getAndAdd(5));
        // 加值并获取(i = 5, 结果 i = 0, 返回 0)
        System.out.println(i.addAndGet(-5));
        // 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(i.getAndUpdate(p -> p - 2));
        // 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(i.updateAndGet(p -> p + 2));
        // 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        // getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
        // getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
        System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
        // 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
        // 其中函数中的操作能保证原子,但函数需要无副作用
        System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));

2. 原子引用

为什么需要原子引用类型?

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference
public interface DecimalAccount {
    // 获取余额
    BigDecimal getBalance();
    // 取款
    void withdraw(BigDecimal amount);
    /**
     * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
     * 如果初始余额为 10000 那么正确的结果应当是 0
     */
    static void demo(DecimalAccount account) {
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            ts.add(new Thread(() -> {
                account.withdraw(BigDecimal.TEN);
            }));
        }
        ts.forEach(Thread::start);
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(account.getBalance());
    }
}

class DecimalAccountUnsafe implements DecimalAccount {
    BigDecimal balance;
    public DecimalAccountUnsafe(BigDecimal balance) {
        this.balance = balance;
    }
    @Override
    public BigDecimal getBalance() {
        return balance;
    }
    @Override
    public void withdraw(BigDecimal amount) {
        BigDecimal balance = this.getBalance();
        this.balance = balance.subtract(amount);
    }
}

实现类加锁实现线程安全:

class DecimalAccountSafeLock implements DecimalAccount {
    private final Object lock = new Object();
    BigDecimal balance;
    public DecimalAccountSafeLock(BigDecimal balance) {
        this.balance = balance;
    }
    @Override
    public BigDecimal getBalance() {
        return balance;
    }
    @Override
    public void withdraw(BigDecimal amount) {
        synchronized (lock) {
            BigDecimal balance = this.getBalance();
            this.balance = balance.subtract(amount);
        }
    }
}

使用 CAS 线程安全:

class DecimalAccountSafeCas implements DecimalAccount {
    AtomicReference<BigDecimal> ref;
    public DecimalAccountSafeCas(BigDecimal balance) {
        ref = new AtomicReference<>(balance);
    }
    @Override
    public BigDecimal getBalance() {
        return ref.get();
    }
    @Override
    public void withdraw(BigDecimal amount) {
        while (true) {
            BigDecimal prev = ref.get();
            BigDecimal next = prev.subtract(amount);
            if (ref.compareAndSet(prev, next)) {
                break;
            }
        }
    }
}

3. ABA 问题及解决

看下面这个代码

 static AtomicReference<String> ref = new AtomicReference<>("A");
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start...");
        // 获取值 A
        // 这个共享变量被它线程修改过?
        String prev = ref.get();
        other();
        Thread.sleep(1000);
        // 尝试改为 C
        System.out.println("change A->C   " + ref.compareAndSet(prev,"C"));
    }
    private static void other() throws InterruptedException {
        new Thread(() -> {
            System.out.println("change A->B    " + ref.compareAndSet(ref.get(),"B"));
        }, "t1").start();
        Thread.sleep(500);
        new Thread(() -> {
            System.out.println("change B->A   " + ref.compareAndSet(ref.get(),"A"));
        }, "t2").start();
    }

运行结果:

main start...
change A->B    true
change B->A   true
change A->C   true

主线程仅能判断出共享变量的值 与 最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程希望:
只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号来实现

AtomicStampedReference 实现:

static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start...");
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        System.out.println("版本:  " + stamp);
        // 如果中间有其它线程干扰,发生了 ABA 现象
        other();
        Thread.sleep(1000);
        // 尝试改为 C
        System.out.println("change A->C " + ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }
    private static void other() throws InterruptedException {
        new Thread(() -> {
            boolean isOk = ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1);
            System.out.println("change A->B "+ isOk);
            System.out.println("更新版本为 "+ref.getStamp());
        }, "t1").start();
        Thread.sleep(500);
        new Thread(() -> {
            System.out.println("change B->A   " + ref.compareAndSet(ref.getReference(), "A",
                    ref.getStamp(), ref.getStamp() + 1));

            System.out.println("更新版本为 " + ref.getStamp());
        }, "t2").start();
    }

AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A ->C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次
但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了
AtomicMarkableReference,如下面这个案例:
在这里插入图片描述

AtomicMarkableReference实现 :

@Slf4j
public class TestABAAtomicMarkableReference {
    public static void main(String[] args) throws InterruptedException {
        GarbageBag bag = new GarbageBag("装满了垃圾");
        // 参数2 mark 可以看作一个标记,表示垃圾袋满了
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
        System.out.println("主线程 start...");
        GarbageBag prev = ref.getReference();
        System.out.println(prev.toString());
        new Thread(() -> {
            System.out.println("打扫卫生的线程 start...");
            bag.setDesc("空垃圾袋");
            boolean res = ref.compareAndSet(bag, bag, true, false);
            System.out.println("打扫卫生的线程 更换了吗"+res);

            System.out.println(bag.toString());
        }).start();
        Thread.sleep(1000);
        System.out.println("主线程想换一只新垃圾袋?");
        boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
        System.out.println("换了么?" + success);
        System.out.println(ref.getReference().toString());
    }
}

class GarbageBag {
    String desc;
    public GarbageBag(String desc) {
        this.desc = desc;
    }
    public void setDesc(String desc) {
        this.desc = desc;
    }
    @Override
    public String toString() {
        return super.toString() + " " + desc;
    }
}

4. 原子数组

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray
/**
     参数1,提供数组 (无参 有结果)
     参数2,获取数组长度的方法(他的参数是 参数1的结果,结果是这个数组的 长度)
     参数3,自增方法,回传 array, index
     参数4,打印数组的方法
     */
    // supplier 提供者 无中生有 ()->结果
    // function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
    // consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
    private static <T> void demo(Supplier<T> arraySupplier,Function<T, Integer> lengthFun,BiConsumer<T, Integer> putConsumer,Consumer<T> printConsumer ) {
        List<Thread> ts = new ArrayList<>();
        T array = arraySupplier.get();
        int length = lengthFun.apply(array);
        for (int i = 0; i < length; i++) {
            // 每个线程对数组作 10000 次操作
            ts.add(new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    putConsumer.accept(array, j%length);
                }
            }));
        }
        ts.forEach(t -> t.start()); // 启动所有线程
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }); // 等所有线程结束
        printConsumer.accept(array);
    }

不安全的数组传递:

       demo(
                ()->new int[10],
                (array)->array.length,
                (array, index) -> array[index]++,
                array-> System.out.println(Arrays.toString(array))
        );

结果:

[9329, 9350, 9206, 9142, 9090, 9079, 9029, 9073, 9067, 9071]

替换成线程安全的数组:

demo(
 ()-> new AtomicIntegerArray(10),
 (array) -> array.length(),
 (array, index) -> array.getAndIncrement(index),
 array -> System.out.println(array)
);

结果:

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

5. 字段更新器

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更数据和数据的版本号,可以解决使用CAS进行原子更新时,可能出现的ABA问题

利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常

Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type

AtomicIntegerFieldUpdater 原子更新字段类案例

不同于以往的几个原子更新类,对于 AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束,约束如下:

  1. 字段必须是 volatile 类型的,在线程之间共享变量时保证立即可见。

  2. 字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。

  3. 只能是实例变量,不能是类变量,也就是说不能加static关键字

  4. 只能是可修改变量,不能是final变量,因为final的语句就是不可修改。实际上final的语义与volatile是有冲突的,两个关键字不能同时存在。

  5. 对于 AtomicIntegerFieldUpdate 和 AtomicLongFieldUpdate 只能修改 int/long 类型的字段,不能修改包装类型。如果要修改包装类型就需要使用 AtomicReferenceFieldUpdate

我们先来看 AtomicIntegerFieldUpdate 的方法列表

// 受保护的无操作构造方法,供子类使用。
protected AtomicIntFieldUpdater()

// 以原子方式将给定值添加到此更新器管理的给定对象的字段的当前值。
int addAndGet(T obj, int delta)
// 如果当前值 == 预期值,则以原子方式将此更新器所管理的给定对象的字段设置为给定的更新值。
abstract boolean compareAndSet(T obj, int expect, int update)
// 以原子方式将此更新器管理的给定对象字段当前值减 1。
int decrementAndGet(T obj)
// 获取此更新器管理的在给定对象的字段中保持的当前值。
abstract int get(T obj)
// 以原子方式将给定值添加到此更新器管理的给定对象的字段的当前值。
int getAndAdd(T obj, int delta)
// 以原子方式将此更新器管理的给定对象字段当前值减 1。
int getAndDecrement(T obj)
// 以原子方式将此更新器管理的给定对象字段的当前值加 1。
int getAndIncrement(T obj)
// 将此更新器管理的给定对象的字段以原子方式设置为给定值,并返回旧值。
int getAndSet(T obj, int newValue)
// 以原子方式将此更新器管理的给定对象字段当前值加 1。
int incrementAndGet(T obj)
// 最后将此更新器管理的给定对象的字段设置为给定更新值。
abstract void lazySet(T obj, int newValue)
// 为对象创建并返回一个具有给定字段的更新器。
static <U> AtomicIntFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName)
// 将此更新器管理的给定对象的字段设置为给定更新值。
abstract void set(T obj, int newValue)
// 如果当前值 == 预期值,则以原子方式将此更新器所管理的给定对象的字段设置为给定的更新值。
abstract boolean weakCompareAndSet(T obj, int expect, int update)

要想原子地更新字段类需要两步。第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新类的字段(属性)必须使用public volatile修饰符

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class Demo {
    // 新建AtomicIntegerFieldUpdater对象,需要指明是哪个类中的哪个字段
    private static AtomicIntegerFieldUpdater<User> atom = AtomicIntegerFieldUpdater.newUpdater(User.class, "id");

    public static void main(String[] args) {
        User user = new User(100, 100);
        System.out.println("调用atom.addAndGet(user, 50)方法返回值为:" + atom.addAndGet(user, 50));
        System.out.println("调用后值变为:" + user);

        System.out.println("调用getAndDecrement(user)方法返回值为:" + atom.getAndDecrement(user));
        System.out.println("调用后值变为:" + user);

        System.out.println("调用compareAndSet(user, 149, 200)方法返回值为:" + atom.compareAndSet(user, 149, 200));
        System.out.println("调用后值变为:" + user);
    }
}
class User {
    volatile int id;
    volatile int age;
    public User(int id, int age) {
        this.id = id;
        this.age = age;
    }

    public String toString() {
        return "id:" + id + " " + "age:" + age;
    }
}

运行结果:

调用atom.addAndGet(user, 50)方法返回值为:150
调用后值变为:id:150 age:100
调用getAndDecrement(user)方法返回值为:150
调用后值变为:id:149 age:100
调用compareAndSet(user, 149, 200)方法返回值为:true
调用后值变为:id:200 age:100

那么 AtomicIntegerFieldUpdater 是如何进行原子性操作的呢?我们去源码一探究竟
先看newUpdater()的源码如下:

public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) {
    Class<?> caller = Reflection.getCallerClass();
    if (AtomicInteger.VM_SUPPORTS_LONG_CAS)
        return new CASUpdater<U>(tclass, fieldName, caller);
    else
        return new LockedUpdater<U>(tclass, fieldName, caller);
}

说明:newUpdater()的作用是获取一个AtomicIntegerFieldUpdater类型的对象
它实际上返回的是CASUpdater对象,或者LockedUpdater对象;具体返回哪一个类取决于JVM是否支持int类型的CAS函数。CASUpdater和LockedUpdater都是AtomicIntegerFieldUpdater的子类,它们的实现类似。下面以CASUpdater来进行说明。

CASUpdater类的源码如下

public boolean compareAndSet(T obj, int expect, int update) {
        if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
        return unsafe.compareAndSwapInt(obj, offset, expect, update);
    }

6. 原子累加器

什么是原子累加器:原子类型累加器是JDK1.8引进的并发新技术,它可以看做AtomicLong和AtomicDouble的部分加强类型
为什么叫部分呢?是因为原子类型累加器适用于数据统计,并不适用于其他粒度的应用

原子类型累加器有如下四种:

  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

LongAdder源码解读:

/**
 * One or more variables that together maintain an initially zero
 * {@code long} sum.  When updates (method {@link #add}) are contended
 * across threads, the set of variables may grow dynamically to reduce
 * contention. Method {@link #sum} (or, equivalently, {@link
 * #longValue}) returns the current total combined across the
 * variables maintaining the sum.
 *
 * <p>This class is usually preferable to {@link AtomicLong} when
 * multiple threads update a common sum that is used for purposes such
 * as collecting statistics, not for fine-grained synchronization
 * control.  Under low update contention, the two classes have similar
 * characteristics. But under high contention, expected throughput of
 * this class is significantly higher, at the expense of higher space
 * consumption.
 *
 * <p>LongAdders can be used with a {@link
 * java.util.concurrent.ConcurrentHashMap} to maintain a scalable
 * frequency map (a form of histogram or multiset). For example, to
 * add a count to a {@code ConcurrentHashMap<String,LongAdder> freqs},
 * initializing if not already present, you can use {@code
 * freqs.computeIfAbsent(k -> new LongAdder()).increment();}
 *
 * <p>This class extends {@link Number}, but does <em>not</em> define
 * methods such as {@code equals}, {@code hashCode} and {@code
 * compareTo} because instances are expected to be mutated, and so are
 * not useful as collection keys.
 *
 * @since 1.8
 * @author Doug Lea
 */
public class LongAdder extends Striped64 implements Serializable {//...}

翻译如下:

一个或者多个变量共同维护一个初始为0的sum值
当多线程之间调用更新方法add()产生竞争时,数据集会动态地进行扩充,以此来减少争用。
sum()方法会返回当前维持sum值的数据集的总和。

当多个线程共同维护一个共享变量进行数据统计时,使用LongAdder的性能要优于AtomicLong。
当然,LongAdder并不适用于更加细粒度的同步控制。
在低并发环境下,这两个类的性能表现是类似的。
但是在高并发环境下,LongAdder会有显著的性能提高,但是也会消耗较高的空间作为牺牲。

LongAdder可以被用于一个ConcurrentHashMap来维持这个可伸缩的数据集。
例如,为ConcurrentHashMap<String,LongAdder> freqs进行增量计算,可以使用freqs.computeIfAbsent(k -> new LongAdder()).increment();

这个类继承自Number类,但是并未定义equals()/hashCode()/compareTo()等方法。
因为实例对象预计是变动的,所以并不适于作为集合类型的Key。

6.1 内部实现思路

原子类型累加器其实是应用了热点分离思想,这一点可以类比一下ConcurrentHashMap的设计思想。

热点分离简述:

  • 将竞争的数据进行分解成多个单元,在每个单元中分别进行数据处理
  • 各单元处理完成之后,通过Hash算法进行计算求和,从而得到最终的结果

热点分离优缺点:

  • 热点分离的设计减小了锁的粒度,提高了高并发环境下的吞吐量。
  • 热点分离的设计需要划分额外的空间进行单元数据的存储,增大了空间消耗

6.2 LongAdder——API

  • LongAdder():累加器只有一个无参的构造器,会构造一个sum=0的实例对象
  • increment():自增
  • decrement():自减
  • add(delat):增量计算
  • sum():计算sum的和
  • reset():重置sum为0
  • sumThenReset():计算sum的和并且重置sum为0
  • intValue():获取sum的int形式(向下转型)
  • floatValue():获取sum的float形式(向上转型)
  • doubleValue():获取sum的double形式(向上转型)

实例代码:

/*
 LongAdder所使用的思想就是热点分离,这一点可以类比一下ConcurrentHashMap的设计思想。
 就是将value值分离成一个数组,当多线程访问时,通过hash算法映射到其中的一个数组进行计数。
 而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度.

 1.LongAdder和LongAccumulator是AtomicLong的扩展
 2.DoubleAdder和DoubleAccumulator是AtomicDouble的扩展
 3.在低并发环境下性能相似;在高并发环境下---吞吐量增加,但是空间消耗增大
 4.多用于收集统计数据,而非细粒度计算
  */

//构造器
LongAdder adder = new LongAdder();
System.out.println("默认构造器:" + adder);

//自增
adder.increment();
System.out.println("increment():自增----" + adder);

//自减
adder.decrement();
System.out.println("decrement():自减----" + adder);

//增量计算
System.out.println("------------add(long):增量计算:");
int sum = 0;
long add;
for (int i = 0; i < 5; i++) {
    add = RandomUtils.nextLong(100, 300);
    sum += add;
    adder.add(add);
    System.out.println("增加---" + add + "-->" + sum);
}

//最终的值
System.out.println("sum():最终值---" + adder.sum());

//重置sum值
adder.reset();
System.out.println("reset():重置值---" + adder);

//获得最终的值并重置
System.out.println("------------再次增量计算:");
sum = 0;
for (int i = 0; i < 5; i++) {
    add = RandomUtils.nextLong(100, 300);
    sum += add;
    adder.add(add);
    System.out.println("增加---" + add + "-->" + sum);
}
System.out.println("sumThenReset():获取最终值并重置---" + adder.sumThenReset());
System.out.println("重置值---" + adder);

//多种形式返回值
System.out.println("------------多种数据类型返回值:");
adder.add(RandomUtils.nextLong(100, 200));
System.out.println("int类型:" + adder.intValue());
System.out.println("double类型:" + adder.doubleValue());
System.out.println("float类型:" + adder.floatValue());

运行结果:

默认构造器:0
increment():自增----1
decrement():自减----0
------------add(long):增量计算:
增加---280-->280
增加---250-->530
增加---252-->782
增加---164-->946
增加---221-->1167
sum():最终值---1167
reset():重置值---0
------------再次增量计算:
增加---269-->269
增加---127-->396
增加---252-->648
增加---107-->755
增加---161-->916
sumThenReset():获取最终值并重置---916
重置值---0
------------多种数据类型返回值:
int类型:164
double类型:164.0
float类型:164.0

6.3 LongAdder 源码分析

LongAdder继承自Striped64抽象类,Striped64中定义了Cell内部类和各重要属性。

6.3.1 主要内部类
// Striped64中的内部类,使用@sun.misc.Contended注解,说明里面的值消除伪共享
@sun.misc.Contended static final class Cell {
    // 存储元素的值,使用volatile修饰保证可见性
    volatile long value;
    Cell(long x) { value = x; }
    // CAS更新value的值
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

    // Unsafe实例
    private static final sun.misc.Unsafe UNSAFE;
    // value字段的偏移量
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

Cell类使用@sun.misc.Contended注解(),说明是要避免伪共享的。
使用Unsafe的CAS更新value的值,其中value的值使用volatile修饰,保证可见性

6.3.2 add(x)方法

add(x)方法是LongAdder的主要方法,使用它可以使LongAdder中存储的值增加x,x可为正可为负

public void add(long x) {
    // as是Striped64中的cells属性
    // b是Striped64中的base属性
    // v是当前线程hash到的Cell中存储的值
    // m是cells的长度减1,hash时作为掩码使用
    // a是当前线程hash到的Cell
    Cell[] as; long b, v; int m; Cell a;
    // 条件1:cells不为空,说明出现过竞争,cells已经创建
    // 条件2:cas操作base失败,说明其它线程先一步修改了base,正在出现竞争
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        // true表示当前竞争还不激烈
        // false表示竞争激烈,多个线程hash到同一个Cell,可能要扩容
        boolean uncontended = true;
        // 条件1:cells为空,说明正在出现竞争,上面是从条件2过来的
        // 条件2:应该不会出现
        // 条件3:当前线程所在的Cell为空,说明当前线程还没有更新过Cell,应初始化一个Cell
        // 条件4:更新当前线程所在的Cell失败,说明现在竞争很激烈,多个线程hash到了同一个Cell,应扩容
        if (as == null || (m = as.length - 1) < 0 ||
            // getProbe()方法返回的是线程中的threadLocalRandomProbe字段
            // 它是通过随机数生成的一个值,对于一个确定的线程这个值是固定的
            // 除非刻意修改它
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            // 调用Striped64中的方法处理
            longAccumulate(x, null, uncontended);
    }
}
  1. 最初无竞争时只更新base
  2. 直到更新base失败时,创建cells数组
  3. 多个线程竞争同一个Cell比较激烈时,可能要扩容
6.3.3 longAccumulate()方法
final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
    // 存储线程的probe值
    int h;
    // 如果getProbe()方法返回0,说明随机数未初始化
    if ((h = getProbe()) == 0) {
        // 强制初始化
        ThreadLocalRandom.current(); // force initialization
        // 重新获取probe值
        h = getProbe();
        // 都未初始化,肯定还不存在竞争激烈
        wasUncontended = true;
    }
    // 是否发生碰撞
    boolean collide = false;                // True if last slot nonempty
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        // cells已经初始化过
        if ((as = cells) != null && (n = as.length) > 0) {
            // 当前线程所在的Cell未初始化
            if ((a = as[(n - 1) & h]) == null) {
                // 当前无其它线程在创建或扩容cells,也没有线程在创建Cell
                if (cellsBusy == 0) {       // Try to attach new Cell
                    // 新建一个Cell,值为当前需要增加的值
                    Cell r = new Cell(x);   // Optimistically create
                    // 再次检测cellsBusy,并尝试更新它为1
                    // 相当于当前线程加锁
                    if (cellsBusy == 0 && casCellsBusy()) {
                        // 是否创建成功
                        boolean created = false;
                        try {               // Recheck under lock
                            Cell[] rs; int m, j;
                            // 重新获取cells,并找到当前线程hash到cells数组中的位置
                            // 这里一定要重新获取cells,因为as并不在锁定范围内
                            // 有可能已经扩容了,这里要重新获取
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                // 把上面新建的Cell放在cells的j位置处
                                rs[j] = r;
                                // 创建成功
                                created = true;
                            }
                        } finally {
                            // 相当于释放锁
                            cellsBusy = 0;
                        }
                        // 创建成功了就返回
                        // 值已经放在新建的Cell里面了
                        if (created)
                            break;
                        continue;           // Slot is now non-empty
                    }
                }
                // 标记当前未出现冲突
                collide = false;
            }
            // 当前线程所在的Cell不为空,且更新失败了
            // 这里简单地设为true,相当于简单地自旋一次
            // 通过下面的语句修改线程的probe再重新尝试
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            // 再次尝试CAS更新当前线程所在Cell的值,如果成功了就返回
            else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                         fn.applyAsLong(v, x))))
                break;
            // 如果cells数组的长度达到了CPU核心数,或者cells扩容了
            // 设置collide为false并通过下面的语句修改线程的probe再重新尝试
            else if (n >= NCPU || cells != as)
                collide = false;            // At max size or stale
            // 上上个elseif都更新失败了,且上个条件不成立,说明出现冲突了
            else if (!collide)
                collide = true;
            // 明确出现冲突了,尝试占有锁,并扩容
            else if (cellsBusy == 0 && casCellsBusy()) {
                try {
                    // 检查是否有其它线程已经扩容过了
                    if (cells == as) {      // Expand table unless stale
                        // 新数组为原数组的两倍
                        Cell[] rs = new Cell[n << 1];
                        // 把旧数组元素拷贝到新数组中
                        for (int i = 0; i < n; ++i)
                            rs[i] = as[i];
                        // 重新赋值cells为新数组
                        cells = rs;
                    }
                } finally {
                    // 释放锁
                    cellsBusy = 0;
                }
                // 已解决冲突
                collide = false;
                // 使用扩容后的新数组重新尝试
                continue;                   // Retry with expanded table
            }
            // 更新失败或者达到了CPU核心数,重新生成probe,并重试
            h = advanceProbe(h);
        }
        // 未初始化过cells数组,尝试占有锁并初始化cells数组
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            // 是否初始化成功
            boolean init = false;
            try {                           // Initialize table
                // 检测是否有其它线程初始化过
                if (cells == as) {
                    // 新建一个大小为2的Cell数组
                    Cell[] rs = new Cell[2];
                    // 找到当前线程hash到数组中的位置并创建其对应的Cell
                    rs[h & 1] = new Cell(x);
                    // 赋值给cells数组
                    cells = rs;
                    // 初始化成功
                    init = true;
                }
            } finally {
                // 释放锁
                cellsBusy = 0;
            }
            // 初始化成功直接返回
            // 因为增加的值已经同时创建到Cell中了
            if (init)
                break;
        }
        // 如果有其它线程在初始化cells数组中,就尝试更新base
        // 如果成功了就返回
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}
  1. 如果cells数组未初始化,当前线程会尝试占有cellsBusy锁并创建cells数组
  2. 如果当前线程尝试创建cells数组时,发现有其它线程已经在创建了,就尝试更新base,如果成功就返回
  3. 通过线程的probe值找到当前线程应该更新cells数组中的哪个Cell
  4. 如果当前线程所在的Cell未初始化,就占有占有cellsBusy锁并在相应的位置创建一个Cell;
  5. 尝试CAS更新当前线程所在的Cell,如果成功就返回,如果失败说明出现冲突;
  6. 当前线程更新Cell失败后并不是立即扩容,而是尝试更新probe值后再重试一次;77. 果在重试的时候还是更新失败,就扩容
  7. 扩容时当前线程占有cellsBusy锁,并把数组容量扩大到两倍,再迁移原cells数组中元素到新数组中
  8. ellsBusy在创建cells数组、创建Cell、扩容cells数组三个地方用到
6.3.4 sum()方法

sum()方法是获取LongAdder中真正存储的值的大小,通过把base和所有段相加得到。

public long sum() {
    Cell[] as = cells; Cell a;
    // sum初始等于base
    long sum = base;
    // 如果cells不为空
    if (as != null) {
        // 遍历所有的Cell
        for (int i = 0; i < as.length; ++i) {
            // 如果所在的Cell不为空,就把它的value累加到sum中
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    // 返回sum
    return sum;
}

可以看到sum()方法是把base和所有段的值相加得到,那么,这里有一个问题,如果前面已经累加到sum上的Cell的value有修改,不是就没法计算到了么?

答案确实如此,所以LongAdder可以说不是强一致性的,它是最终一致性的。

6.4 高并发性能测试 LongAdder对比AtomicLong

   public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            demo(() -> new LongAdder(), adder -> adder.increment());
        }
        for (int i = 0; i < 5; i++) {
            demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
        }

    }
    private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();
        // 4 个线程,每人累加 50 万
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start)/1000_000);
    }

运行结果:
在这里插入图片描述

性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

猜你喜欢

转载自blog.csdn.net/haiyanghan/article/details/109435620
今日推荐