Java并发编程学习(8):CAS机制、原子变量

示例引入

我们需要执行一个高并发削减账户余额的逻辑。为方便起见,我们将账户类设计为一个抽象类,并且只对中的静态demo()方法进行了实现。
demo()方法中,我们会生成若干线程,每个线程执行同样的逻辑:扣除账户中的余额。当全部线程执行完毕后,方法会打印账户的余额数量以及执行程序的用时。

abstract class Account{


    abstract String getName();

    abstract int getBalance();

    abstract void withdraw(int amount);

    static void demo(Account account, int listNum, int preAmount){

        // 启动时间延迟5秒,确保所有线程全部启动,同时执行
        long start = System.currentTimeMillis() + 5000;

        Runnable task = () -> {
            try {
                Thread.sleep(start - System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i = 0; i < 10; i++) {
                account.withdraw(preAmount);
            }
        };
        
        List<Thread> threadList = new ArrayList<>();
        for (int i = 0; i < listNum; i++) {
            threadList.add(new Thread(task, "t"+i));
        }

        // 启动所有线程
        threadList.forEach(Thread::start);
        // 等待所有线程结束
        threadList.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.currentTimeMillis();

        log.info("{} : balance = {}, use {} ms",account.getName(),account.getBalance(), end - start);
    }
}

不加锁的实现方式

此方法没有在扣除余额时加锁,可能会造成线程安全问题。

class UnsafeAccount extends Account{

    private int balance;

    public UnsafeAccount(int balance) {
        this.balance = balance;
    }

    @Override
    String getName() {
        return "Unsafe";
    }

    @Override
    int getBalance() {
        return balance;
    }

    @Override
    void withdraw(int amount) {
        balance -= amount;
    }
}

使用synchronized进行加锁

使用synchronized对变量balance的读写进行加锁,可以保证线程安全。

class SynchronizedAccount extends Account{
    private int balance;

    public SynchronizedAccount(int balance) {
        this.balance = balance;
    }

    @Override
    String getName() {
        return "Synchronize";
    }

    @Override
    synchronized int getBalance() {
        return balance;
    }

    @Override
    synchronized void withdraw(int amount) {
        balance -= amount;
    }
}

使用原子变量

原子变量底层使用CAS机制,它能够保证共享数据在多线程间安全的同时,也无需加锁。

class CasAccount extends Account{
    private AtomicInteger balance;

    public CasAccount(int balance) {
        this.balance = new AtomicInteger(balance);
    }

    @Override
    String getName() {
        return "Cas";
    }

    @Override
    int getBalance() {
        return balance.get();
    }

    @Override
    void withdraw(int amount) {
        balance.addAndGet(-amount);
    }
}

实验

我们在主函数中定义初始余额为50000,一共创建5000个线程去扣减余额,每次扣减的数额为1。由于每个线程会扣减10次,因此最后正确的余额应该为0才对。

public class AccountSubDemo{
    public static void main(String[] args) {

        int initBalance = 50000;
        int listNum = 5000;
        int preAmount = 1;

        Account unsafeAccount = new UnsafeAccount(initBalance);
        Account.demo(unsafeAccount,listNum,preAmount);

        Account synchronizedAccount = new SynchronizedAccount(initBalance);
        Account.demo(synchronizedAccount,listNum,preAmount);

        Account casAccount = new CasAccount(initBalance);
        Account.demo(casAccount,listNum,preAmount);
    }
}

下面是函数的执行结果。

[5290 ms] [INFO][main] i.k.e.c.ex.Account : Unsafe : balance = 122, use 138 ms
[10641 ms] [INFO][main] i.k.e.c.ex.Account : Synchronize : balance = 0, use 348 ms
[15715 ms] [INFO][main] i.k.e.c.ex.Account : Cas : balance = 0, use 74 ms

从结果中容易看出,未加锁时,会出现线程安全问题,导致最终余额不为0。使用synchronized加锁的代码,能够正确执行结果,但是它的耗时比不加锁的长了一倍。使用原子变量的代码,在能够正确执行结果的同时,其耗时也十分优秀。

CAS工作方式

在前面原子变量的解决方案中,其方法内部并不是通过加锁来保护共享变量的线程安全的。其内部保护线程安全的关键为CAS(Compare And Set或者Compare And Swap),该操作必须是原子的。
cas需要两个重要参数:exceptedValue和newValue。当且仅当目标变量的值为exceptedValue时,他才会将其修改为newValue。下面通过一个例子进行展示。

线程1 Account对象 线程2 获取余额100(exceptedValue) 100-1=99(newValue) 已经修改为99 cas(100,99) 修改失败 获取余额99(exceptedValue) 99-1=98(newValue) cas(99,98) 修改成功 线程1 Account对象 线程2

CAS底层原理

CAS的底层是lock cmpxch指令(X86架构),在单核CPU和多核CPU下都能够保证CAS的原子性。
在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,因此是原子的。

CAS与volital

CAS操作需要volital的支持:获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。
CAS必须借助volatile才能读取到共享变量的最新值来实现比较-交换操作。

CAS的效率分析

  • 无锁情况下,即使重试失败,线程依然在高速运行,没有停歇。
  • 而synchronized会让线程在没有获得锁的时候发生上下文切换,进入阻塞。

另一方面:

  • 无锁情况下,因为线程要保持运行,需要额外CPU的支持,如果线程没有分到时间片,仍然会发生上下文切换。

因此:

  • 要想最大限度发挥CAS的优势,必须要多线程支持,而且线程数少于CPU的核心数

特点

CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改贡献变量,就算改了也没有关系,自己吃亏再重试就行。
synchronized是基于悲观锁的思想:最悲观的估计,得防着其他变量来修改共享变量,我上了锁你们都别想该,我用完了解开锁,别人才有机会。
CAS是无锁并发,无阻塞并发。

  • 无锁并发:不是使用加锁的方式来保护共享资源,而是采用不断重试的方式来保证共享资源的正确性;
  • 无阻塞并发:线程可以持续运行,不会等待其它线程完成或者释放锁。

原子变量

在JUC包下,Java基于CAS提供了一些原子变量工具类。

原子整数

原子整数包括AtomicBooleanAtomicIntegerAtomicLong。由于它们功能比较类似,这里选取AtomicInteger进行讲解。

基础CAS操作

AtomicInteger i = new AtomicInteger(0);
// 只有当i为0时,才将其赋值为1
i.compareAndSet(0,1);

CAS封装的简单API

i.getAndIncrement(); // 等价于 i++

i.incrementAndGet(); // 等价于 ++i

i.getAndDecrement(); // 等价于 i--

i.decrementAndGet(); // 等价于 --i

i.getAndAdd(5);// 先取值,再求和

i.addAndGet(5);// 先求和,再取值

CAS封装的复杂API

上面的简单API只能完成加法、减法的操作,如果业务需要跟复杂的计算(比如乘除法),则需要使用getAndUpdate()getAndAccumulate()

方法getAndUpdate()的签名如下,它需要传入一个IntUnaryOperator对象。

public final int getAndUpdate(IntUnaryOperator updateFunction)

点进IntUnaryOperator类中,可以发现它是一个注解为@FunctionalInterface接口,需要实现的方法为int applyAsInt(int operand);。因此,我们可以配合lambda表达式使用方法getAndUpdate()

举例:例如,我们希望让结果乘2,可以按照如下的方式使用

i.getAndUpdate(a -> a*2);

方法getAndUpdate()的签名如下,它需要传入一个IntUnaryOperator对象。

public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)

点进IntBinaryOperator类中,可以发现它是一个注解为@FunctionalInterface接口,需要实现的方法为int applyAsInt(int left, int right);。因此,我们可以配合lambda表达式使用方法getAndAccumulate()。这里表示要想计算最终结果,还需要另一个参数x

举例:例如,我们希望让结果乘x,可以按照如下的方式使用

i.getAndAccumulate(3, (pre,x) -> pre*x);

注意:再运算时,原数值pre在左,新参数x在右。

如果计算需要的参数超过一个,改怎么办?老老实实用compareAndSet()方法吧。
举例:例如,我们希望让结果乘x再加上y,可以按照如下的方式使用

while (true){
    int pre = i.get();
    int newValue = pre*x+y;
    if (i.compareAndSet(pre,newValue)){
        break;
    }
}

此外,getAndUpdate()getAndAccumulate()返回的均为计算前的数值,而updateAndGet()accumulateAndGet()返回的均为计算后的数值

原子引用

有时,被保护的数据类型不一定是基本数据类型,也有可能是类似BigDecimal这样的数据类型。此时,可以使用原子引用来保护其中的共享变量。
原子引用主要有:AtomicReferenceAtomicStampedReferenceAtomicMarkableReference
这里选取AtomicReference进行讲解。

初始化

AtomicReference初始化时,最好将原始数据传入进去。

BigDecimal decimal = new BigDecimal("11.11");
AtomicReference<BigDecimal> reference = new AtomicReference<>(decimal);

基础CAS操作

基础CAS操作与AtomicInteger基本相同。但是这里需要注意,由于引用类型与基本数据类型不同,在compareAndSet()中,比较的是引用地址,即只有引用地址相等才认为两者相等。

while (true) {
    BigDecimal preValue = reference.get();
    BigDecimal newValue = preValue.subtract(new BigDecimal("2.22"));
    if(reference.compareAndSet(preValue, newValue)){
        break;
    }
}

下面这段例子深刻体现了只比较引用这一个特点:虽然integerAtomicReference中的值确实是11000,但是由于compareAndSet()时java会为11000创建一个新的对象,导致两者引用并不相同,进而始终不满足修改条件。

Integer integer = 11000;
AtomicReference<Integer> integerAtomicReference = new AtomicReference<>(integer);

while (true) {
    if(integerAtomicReference.compareAndSet(11000, 22222)){
        break;
    }
}

CAS封装的API

AtomicInteger相同,AtomicReference也存在getAndUpdate()getAndAccumulate()方法,只是它们传入的操作函数变为了UnaryOperator<V>BinaryOperator<V>类型,其中泛型V为构造AtomicReference时传入的类型。

ABA问题

从之前的案例可以看出,AtomicReference在执行compareAndSet()时会比较引用,但是无法感知到手否有其它线程修改过该变量。
在下面的例子中,主线程希望将A改为C,但是在other()方法中,他先将A改为B,又将B改为A,实际上变量已经发生了变动,但是主线程感知不到。

public class AtomicReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicReference<String> reference = new AtomicReference<>("A");

        String pre = reference.get();
        other(reference);
        boolean a2c = reference.compareAndSet(pre, "C");
        log.info("A -> C : {}",a2c);
        
    }

    private static void other(AtomicReference<String> reference) {
        boolean a2b = reference.compareAndSet(reference.get(), "B");
        log.info("A -> B : {}",a2b);
        boolean b2a = reference.compareAndSet(reference.get(), "A");
        log.info("B -> A : {}",b2a);
    }
}

日志打印结果如下

[164 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : A -> B : true
[168 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : B -> A : true
[168 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : A -> C : true

带版本号的原子引用

只要有其它线程动过了该共享变量,那么自己的CAS就会失败。此时,仅比较引用是不过的,还需要一个版本号。Java中AtomicStampedReference可以完成此功能。
AtomicStampedReference中,可以通过getReference()获取引用,通过getStamp()获取版本号。此外,在初始化时,除了传递引用外,还需要设置一个初始版本号。
同时,在修改时,除了传新旧引用外,还需要传递新旧版本号。
我们可以利用AtomicStampedReference对ABA问题进行修改。

public class AtomicReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        AtomicStampedReference<String> reference = new AtomicStampedReference<String>("A",0);
        
        int stamp = reference.getStamp();
        String pre = reference.getReference();
        other(reference);
        boolean a2c = reference.compareAndSet(pre, "C",stamp,stamp + 1);
        log.info("A -> C : {}",a2c);

    }

    private static void other(AtomicStampedReference<String> reference) {
        int stamp1 = reference.getStamp();
        boolean a2b = reference.compareAndSet(reference.getReference(), "B",stamp1, stamp1 + 1);
        log.info("A -> B : {}",a2b);
        int stamp2 = reference.getStamp();
        boolean b2a = reference.compareAndSet(reference.getReference(), "A",stamp2, stamp2 + 1);
        log.info("B -> A : {}",b2a);
    }

}

此时日志打印结果如下,A到C的修改已经不能成功

[157 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : A -> B : true
[160 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : B -> A : true
[161 ms] [INFO][main] i.k.e.c.e.a.AtomicReferenceDemo : A -> C : false

带标记的原子引用

有时,我们并不关心引用变量更改了几次,只单纯关心它是否被更改过,此时我们可以使用AtomicMarkableReference
AtomicMarkableReference中,标记位并非整数,而是一个boolean值。

原子数组

由于原子引用在执行compareAndSet()时只会比较引用,因此它无法对对象里面的内容进行线程安全的保护。针对数组,java在JUC中为我们提供了原子数组来解决这一问题。
原子数组包括AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray

使用示例

为了统一测试,我们可以使用函数式接口来统一执行流程,而具体逻辑则在测试方法外部实现。

private static <T> void demo(
        Supplier<T> arraySupplier,
        Function<T, Integer> lengthFunction,
        BiConsumer<T, Integer> consumer,
        Consumer<T> printer
) throws InterruptedException {
    ArrayList<Thread> threads = new ArrayList<>();
    // 创建数组
    T array = arraySupplier.get();
    // 获取数组长度
    Integer length = lengthFunction.apply(array);
    for (int i = 0; i < length ;i++) {
          // 每个线程将10000次自增均摊到每个元素上(共享变量没有锁保护)
         threads.add(new Thread(() -> {
             for (int j = 0; j < 10000; j++) {
                 consumer.accept(array, j%length);
             }
         },"t-"+i));
    }
    // 启动并等待线程
    threads.forEach(Thread::start);
    for (Thread thread : threads) {
        thread.join();
    }
    // 打印数组元素
    printer.accept(array);
}

demo()函数中,我们将10000次逻辑均摊到了数组的每个元素上,而且线程执行过程中并没有加锁。
下面我们可以对比常规数组和原子数组在demo()上的表现。

public static void main(String[] args) throws InterruptedException {
    demo(
            () -> new int[10],
            (array) -> array.length,
            (array,index) -> array[index]++,
            (array) -> System.out.println(Arrays.toString(array))
    );

    demo(
            () -> new AtomicIntegerArray(10),
            (array) -> array.length(),
            (array,index) -> array.getAndUpdate(index, i -> i+1),
            (array) -> System.out.println(array)
    );
}

输出结果如下所示,可见,常规数组因为线程安全问题,其每个元素的最终结果并没有达到10000

[8396, 8423, 8416, 8424, 8438, 8432, 8421, 8414, 8449, 8449]
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]

字段更新器

原子数组保护的是数组中的元素,字段更新器保护的是某个对象的成员变量。
字段更新器包括AtomicReferenceFieldUpdaterAtomicIntegerFieldUpdaterAtomicLongFieldUpdater

初始化

字段更新器的初始化与其它原子变量不同,它们不针对某一个被保护的对象,AtomicIntegerFieldUpdaterAtomicLongFieldUpdater的初始化方法如下

newUpdater(Class<U> tclass, String fieldName)

其中tclass表示需要保护的字段的所在类,fieldName需要保护的字段的名称,该字段必须为整形或长整型。
而对于AtomicReferenceFieldUpdater,其初始化方法为

newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)

其中,vclass表示字段本身的类型,其它两个的含义与AtomicIntegerFieldUpdater相同。

注意:由于被保护的字段需要在多个线程之间共享,因此它应该被设置为volatile的,同时,由于字段需要被其他类访问,因此它不应该设置为private

CAS操作

由于字段更新器在初始化时不针对某一个被保护的对象,因此在CAS时需要将相应的对象传入。
下面的代码是一段使用示例,它通过CAS的方式修改了一个学生的名字。

public class AtomicFeildDemo {
    public static void main(String[] args) {
        AtomicReferenceFieldUpdater<Student, String> nameUpdater =
                AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        Student student = new Student("小明");
        System.out.println(student);
        boolean b = nameUpdater.compareAndSet(student, student.getName(), "小李");
        System.out.println("小明 -> 小李 : "+b);
        System.out.println(student);
    }
}
class Student{
    protected volatile String name;

    public Student(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Student{");
        sb.append("name='").append(name).append('\'');
        sb.append('}');
        return sb.toString();
    }
}

原子累加器

指对整数进行累加操作。
JDK1.8后,新增了几个专门用于累加的原子类,它的性能要比传统的原子整形更好。
原子累加器包括:LongAdderDoubleAdder。这里以LongAdder为例。

初始化

LongAdder进行初始化时,无需传任何参数,其默认起始值为0,如果需要修改起始值,可以在创建后使用add()方法进行修改。

LongAdder longAdder = new LongAdder();
longAdder.add(10L);

常用API

API用法
add(long l)将和加上l
increment()对整形进行自增1
sum()给出当前时刻的和,并不一定完全准确
reset将和重置到0
longValue()sum()

性能比较

为方便比较,我们使用了统一的demo()函数,其中累加器的提供和具体执行由外部实现,并通过函数式接口传递进来。在demo()中,我们会使用5个线程,每个线程对累加器操作2000000次,累加器的最终结果应该为10000000。

    static <T> void demo(
            Supplier<T> adderSupplier,
            Consumer<T> function
    ) throws InterruptedException {
        T adder = adderSupplier.get();
        ArrayList<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 5; i++) {
            threads.add(new Thread(() -> {
                for (int j = 0; j < 2000000; j++) {
                    function.accept(adder);
                }
            },"t-"+i));
        }


        long start = System.currentTimeMillis();
        threads.forEach(Thread::start);

        for (Thread thread : threads) {
            thread.join();
        }

        long end = System.currentTimeMillis();

        log.info("adder: {}, use {} ms",adder,end - start);

    }

为了公平起见,我们同时比较AtomicLongLongAdder,并将它们各执行了10次。

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            demo(
                    () -> new AtomicLong(0),
                    adder -> adder.incrementAndGet()
            );
        }
        log.info("-------------------------");

        for (int i = 0; i < 10; i++) {
            demo(
                    () -> new LongAdder(),
                    adder -> adder.increment()
            );
        }
    }

最终结果如下所示,显然LongAdderAtomicLong有更好的性能表现。

[317 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 164 ms
[490 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 170 ms
[710 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 220 ms
[869 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 159 ms
[1074 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 205 ms
[1230 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 156 ms
[1432 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 202 ms
[1627 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 194 ms
[1832 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 205 ms
[2033 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 201 ms
[2033 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : -------------------------
[2059 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 25 ms
[2078 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 18 ms
[2099 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 20 ms
[2116 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2133 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2151 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2168 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2185 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2202 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 17 ms
[2220 ms] [INFO][main] i.k.e.c.e.a.AdderDemo : adder: 10000000, use 18 ms
性能提升的原因

虽然AtomicLong使用CAS算法,但是CAS失败后还是通过无限循环的自旋锁不多的尝试,这就是高并发下CAS性能低下的原因所在。
高并发下N多线程同时去操作一个变量会造成大量线程CAS失败,然后处于自旋状态,导致严重浪费CPU资源,降低了并发性。既然AtomicLong性能问题是由于过多线程同时去竞争同一个变量的更新而降低的,那么如果把一个变量分解为多个变量,让同样多的线程去竞争多个资源,这样的思想驱动下锁分段理念下的LongAddr孕育而生。

LongAdder源码分析

CAS锁

利用CAS,可以实现加锁与解锁操作。但请勿将其运用于生产实践,因为他对CPU的消耗很大。

public class LockCas {
    private AtomicInteger state = new AtomicInteger(0);
    public void lock() {
        while (true) {
            if (state.compareAndSet(0, 1)) {
                break;
            }
        }
    }
    public void unlock() {
        log.debug("unlock...");
        state.set(0);
    }
}
关键域与Cell类

注意:我们这里以JDK8为例,这里的源码在之后的版本有变化
LongAdder 类有几个关键域

// 累加单元数组, 懒惰初始化
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
// 在 cells 创建或扩容时, 置为 1, 表示加锁
transient volatile int cellsBusy;

其中,volatile保证可见性,transient则表示它们不会被用于序列化。

    // 防止缓存行伪共享
    @sun.misc.Contended 
    static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        final boolean cas(long cmp, long val) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
        }

        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        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);
            }
        }
    }
缓存行伪共享

CPU缓存与内存级别与速度差异
在这里插入图片描述

从 cpu 到大约需要的时钟周期
寄存器1 cycle (4GHz 的 CPU 约为0.25ns)
L13~4 cycle
L210~20 cycle
L340~45 cycle
内存120~240 cycle

因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long)。
但是,缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效。
造成的问题
因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为24字节(16 字节的对象头+ 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:
在这里插入图片描述

  • Core-0 要修改 Cell[0]
  • Core-1 要修改 Cell[1]

无论谁修改成功,都会导致对方 Core 的缓存行失效。

@Contended解决缓存行伪共享

它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
在这里插入图片描述

add()方法解析

LongAdder类中,increment()方法的本质是add(1L),因此我们主要研究add()方法

    public void add(long x) {
        // as 为累加单元数组
        // b 为基础值
        // x 为累加值
        Cell[] as;
        long b, v;
        int m;
        Cell a;
        // 进入 if 的两个条件
        // 1. as 有值, 表示已经发生过竞争, 进入 if
        // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            // uncontended 表示 cell 没有竞争
            boolean uncontended = true;
            if (
                // as 还没有创建
                as == null || (m = as.length - 1) < 0 ||
                // 当前线程对应的 cell 还没有
                (a = as[getProbe() & m]) == null ||
                // cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell )
                !(uncontended = a.cas(v = a.value, v + x))
            ) {
                // 进入 cell 数组创建、cell 创建的流程
                longAccumulate(x, null, uncontended);
            }
        }
    }

流程图

为空
不为空
成功
失败
创建了
成功
失败
没创建
cells
当前线程
cas base 累加
当前线程 cell 是否创建
return
longAccumulate
cas cell累加
longAccumulate()解析
    final void longAccumulate(long x, LongBinaryOperator fn,
                              boolean wasUncontended) {
        int h;
        // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
        if ((h = getProbe()) == 0) {
            // 初始化 probe
            ThreadLocalRandom.current(); // force initialization
            // h 对应新的 probe 值, 用来对应 cell
            h = getProbe();
            wasUncontended = true;
        }
        // collide 为 true 表示需要扩容
        boolean collide = false;                // True if last slot nonempty
        done: for (;;) {
            Cell[] cs; Cell c; int n; long v;
            // 已经有了 cells
            if ((cs = cells) != null && (n = cs.length) > 0) {
                // 还没有 cell
                if ((c = cs[(n - 1) & h]) == null) {
                    // 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
                    // 成功则 break, 否则继续 continue 循环
                    if (cellsBusy == 0) {       // Try to attach new Cell
                        Cell r = new Cell(x);   // Optimistically create
                        if (cellsBusy == 0 && casCellsBusy()) {
                            try {               // Recheck under lock
                                Cell[] rs; int m, j;
                                if ((rs = cells) != null &&
                                        (m = rs.length) > 0 &&
                                        rs[j = (m - 1) & h] == null) {
                                    rs[j] = r;
                                    break done;
                                }
                            } finally {
                                cellsBusy = 0;
                            }
                            continue;           // Slot is now non-empty
                        }
                    }
                    collide = false;
                }
                // 有竞争, 改变线程对应的 cell 来重试 cas
                else if (!wasUncontended)       // CAS already known to fail
                    wasUncontended = true;      // Continue after rehash
                // cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
                else if (c.cas(v = c.value,
                        (fn == null) ? v + x : fn.applyAsLong(v, x)))
                    break;
                // 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
                else if (n >= NCPU || cells != cs)
                    collide = false;            // At max size or stale
                // 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
                else if (!collide)
                    collide = true;
                // 尝试加锁
                else if (cellsBusy == 0 && casCellsBusy()) {
                    // 加锁成功, 扩容
                    try {
                        if (cells == cs)        // Expand table unless stale
                            cells = Arrays.copyOf(cs, n << 1);
                    } finally {
                        cellsBusy = 0;
                    }
                    collide = false;
                    continue;                   // Retry with expanded table
                }
                // 改变线程对应的 cell
                h = advanceProbe(h);
            }
            // 还没有 cells, 尝试给 cellsBusy 加锁
            else if (cellsBusy == 0 && cells == cs && casCellsBusy()) {
                // 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
                // 成功则 break;
                try {                           // Initialize table
                    if (cells == cs) {
                        Cell[] rs = new Cell[2];
                        rs[h & 1] = new Cell(x);
                        cells = rs;
                        break done;
                    }
                } finally {
                    cellsBusy = 0;
                }
            }
            // 上两种情况失败, 尝试给 base 累加
            else if (casBase(v = base,
                    (fn == null) ? v + x : fn.applyAsLong(v, x)))
                break done;
        }
    }

longAccumulate()中,我们可以将代码分为三个主要部分:cells的创建、cell的创建与cas cell。

cells的创建
成功
失败
成功
失败
循环入口
cells不存在&未加锁&未新建
加锁
创建 cells 并初始化一个 cell
return
cas base 累加
cell的创建
成功
成功
失败
失败
循环入口
cells存在&对应槽位上的cell没创建
创建cell
加锁
再次检查对应槽位为空
return
cas cell
成功
失败
失败
成功
循环入口
cells存在&对应槽位上的cell已经创建
return
cells的长度是否超过CPU上限
改变线程对应的cell
加锁
扩容
sum()解析

LongAdder类中,sum()方法会将cells中的所有cell进行累加,并最终加上base的值,作为求和的结果。

    public long sum() {
        Cell[] cs = cells;
        long sum = base;
        if (cs != null) {
            for (Cell c : cs)
                if (c != null)
                    sum += c.value;
        }
        return sum;
    }

Unsafe

Unsafe 对象提供了非常底层的,操作内存、线程的方法,在之前的原子变量中,大量用到了Unsafe 对象来进行内存级别的操作。
但是不要被类的名字所迷惑,这里的“不安全”(unsafe)并非指的是线程不安全,而是指该类过于底层,开发者如果擅自使用不安全。因此,不建议大家擅自使用此类。

Unsafe对象的获取

Unsafe 对象采用了单例模式,但是它既不能创建,也不能通过getUnsafe()方法获取。
如果在代码中直接采用getUnsafe(),会抛出异常java.lang.SecurityException
Unsafe采用了单例模式,其对象存储在内部变量theUnsafe上,我们可以通过反射来获取该对象。

public class UnsafeDemo {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Unsafe unsafe = getUnsafe();
        System.out.println(unsafe);
    }
    
    public static Unsafe getUnsafe() {
        Field field = null;
        try {
            field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Unsafe对象的CAS方法

Unsafe对象的CAS方法有如下3种,每种方法需要4个参数

    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
参数名含义
var1变量所在的对象
var2域(成员变量)的偏移地址
var4原值
var5新值

为了使用CAS,就需要先获得成员变量的偏移地址。使用unsafeobjectFieldOffset()可以获得成员变量的偏移地址。因此,我们可以通过如下方式使用Unsafe对象的CAS方法。

    static class Student{
        int age;
        String name;

        public Student(int age, String name) {
            this.age = age;
            this.name = name;
        }

        @Override
        public String toString() {
            final StringBuffer sb = new StringBuffer("Student{");
            sb.append("age=").append(age);
            sb.append(", name='").append(name).append('\'');
            sb.append('}');
            return sb.toString();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException {
        Unsafe unsafe = getUnsafe();
        if (unsafe == null){
            return;
        }
        long ageOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("age"));
        long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));

        Student student = new Student(20, "小明");
        System.out.println(student);
        unsafe.compareAndSwapInt(student,ageOffset,20,17);
        unsafe.compareAndSwapObject(student,nameOffset,"小明","小李");
        System.out.println(student);
    }

打印结果如下,可见,Unsafe对象成功使用CAS方法修改了student中各成员变量的值。

Student{age=20, name='小明'}
Student{age=17, name='小李'}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值