【JavaEE】CAS原理实现 + 常见应用

本文基于jdk8

参考:

黑马程序员深入学习Java并发编程,JUC并发编程全套教程_哔哩哔哩_bilibili


CAS原理

CAS:比较和交换(设置) Compare And Swap(Set)。当A的值为5的时候,给A设置值为10。这里涉及到的比较和设置 值的操作是原子的。


CAS的操作系统层实现

操作系统层面的CAS是一条CPU的原子指令 (cmpxchg指令),正是由于该指令具备了原子性,因此使用CAS操作数据时不会造成数据不一致的问题。


CAS的Java层实现

Java是把底层C++代码调用的这个原子指令封装到native方法中了。不过还提供了Unsafe类用来操作相关线程等。但这个类无法直接调用,只能通过反射来使用。该类不建议使用

获取Unsafe

java 复制代码
import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class UnsafeAccessor {
    static Unsafe unsafe;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new Error(e);
        }
    }

    static Unsafe getUnsafe() {
        return unsafe;
    }
}

使用Unsafe

java 复制代码
import sun.misc.Unsafe;

import java.lang.reflect.Field;


class Student {
    volatile int id;
    volatile String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

    public static void main(String[] args) throws NoSuchFieldException {
        Unsafe unsafe = UnsafeAccessor.getUnsafe();
        Field id = Student.class.getDeclaredField("id");
        Field name = Student.class.getDeclaredField("name");
        // 获得成员变量的偏移量
        long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);
        long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);
        Student student = new Student();
        // 使用 cas 方法替换成员变量的值
        UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
        UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
        System.out.println(student);
    }
}

CAS中的ABA问题

某个共享变量在线程1中刚获取到值为A,后面要对这个值进行CAS操作,把它变成C。

但是线程1在CAS之前,线程2也读到了共享变量的值,并且它先于线程1对改共享变量的CAS操作,把这个共享变量变成B,最后在线程1在CAS操作之前又把这个变量改成A。

当线程1要执行CAS操作时,它一看还是原来的A,就直接执行了,但实际上并不是原来的A了

java 复制代码
import java.util.concurrent.atomic.AtomicReference;

import static java.lang.Thread.sleep;

public class CasAba {
    static volatile AtomicReference<String> ref = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程获取A");
        // 获取值 A
        // 这个共享变量被它线程修改过?
        String prev = ref.get();
        other();
        sleep(1000);
        // 尝试改为 C
        if (ref.compareAndSet(prev, "C")) {
            System.out.println("主线程修改成功:" + prev + "->" + ref.get());
        }
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            System.out.println("t1线程获取A");
            String prev = ref.get();
            if (ref.compareAndSet(ref.get(), "B")) {
                System.out.println("t1修改成功:" + prev + "->" + ref.get());
            }
        }, "t1").start();

        sleep(500);

        new Thread(() -> {
            System.out.println("t2线程获取B");
            String prev = ref.get();
            if (ref.compareAndSet(ref.get(), "A")) {
                System.out.println("t2修改成功:" + prev + "->" + ref.get());
            }
        }, "t2").start();
    }
}

解决方案

如果比较值不够,那么就在值的基础上在加个版本号。每次读取该值,版本就加一。比较的时候也要看看版本号是不是之前的版本。

AtomicStampedReference

该类就是带版本号的实现。

java 复制代码
import java.util.concurrent.atomic.AtomicStampedReference;

import static java.lang.Thread.sleep;

public class AtomicStampedReferenceTest {
    static volatile AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) throws InterruptedException {
        // 获取值 A
        String prev = ref.getReference();
        // 获取版本号
        int stamp = ref.getStamp();
        System.out.println("主线程获取初始值:" + prev + " 版本号:" + stamp);
        other();
        sleep(1000);
        // 尝试改为 C + 版本号加一
        if (ref.compareAndSet(prev, "C", stamp, stamp + 1)) {
            System.out.println("主线程修改成功:" + prev + "->" + ref.getReference() + " 版本号:" + ref.getStamp());
        } else {
            System.out.println("主线程修改失败:" + prev + "->" + ref.getReference() + " 版本号:" + ref.getStamp());
        }
    }

    private static void other() throws InterruptedException {
        new Thread(() -> {
            String prev = ref.getReference();
            int stamp = ref.getStamp();
            System.out.println("t1线程获取初始值" + prev + " 版本号:" + stamp);
            if (ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1)) {
                System.out.println("t1修改成功:" + prev + "->" + ref.getReference() + " 版本号:" + ref.getStamp());
            }
        }, "t1").start();

        sleep(500);

        new Thread(() -> {
            String prev = ref.getReference();
            int stamp = ref.getStamp();
            System.out.println("t2线程获取初始值" + prev + " 版本号:" + stamp);
            if (ref.compareAndSet(prev, "A", stamp, stamp + 1)) {
                System.out.println("t2修改成功:" + prev + "->" + ref.getReference());
            }
        }, "t2").start();
    }
}

CAS特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,多重试几次

synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。


常见应用

以下这些类或者关键字都使用了CAS。大部分都在juc下

synchronized

synchronized在进行锁升级的时候,基本在每次升级的使用都用到了CAS来设置值。

比如在偏向锁时,把线程ID记录到对象头中,这里的记录用的CAS来记录的;

在轻量级锁时,要把对象头中的Mark Word和锁相关的值记录到锁记录中。

上面这些都是单次的设置值的操作,而在轻量级锁在变成重量级锁之前,会自旋等待,这里就一直是CAS操作。


原子基础类型

对于一些基础类型,juc提供了原子类来方便使用。

java 复制代码
import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomicInteger {
    public static void main(String[] args) {
        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));
    }
}

原子引用类型

可以使用这个类型来把对象的引用给变成原子的。

AtomicReference AtomicMarkableReference AtomicStampedReference

比如上面CAS中的ABA问题中的实例,String的使用方法。


原子数组

对于数组的包装:AtomicIntegerArray AtomicLongArray AtomicReferenceArray

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class TestAtomicArray {

    /**
     * 参数1,提供数组、可以是线程不安全数组或线程安全数组
     * 参数2,获取数组长度的方法
     * 参数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);
    }


    public static void main(String[] args) {
        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.getAndIncrement(index),
                array -> System.out.println(array));
    }
}

原子字段(属性)

对于类中的字段的包装,必须配合volatile使用,不然报异常

AtomicReferenceFieldUpdater AtomicIntegerFieldUpdater AtomicLongFieldUpdater

java 复制代码
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class TestFieldAtomic {
    private volatile int field;
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater fieldUpdater =
                AtomicIntegerFieldUpdater.newUpdater(TestFieldAtomic.class, "field");
        TestFieldAtomic testFieldAtomic = new TestFieldAtomic();
        fieldUpdater.compareAndSet(testFieldAtomic, 0, 10);
        // 修改成功 field = 10
        System.out.println(testFieldAtomic.field);
        // 修改成功 field = 20
        fieldUpdater.compareAndSet(testFieldAtomic, 10, 20);
        System.out.println(testFieldAtomic.field);
        // 修改失败 field = 20
        fieldUpdater.compareAndSet(testFieldAtomic, 10, 30);
        System.out.println(testFieldAtomic.field);
    }
}

原子累加器

原子累加器有两类。其中LongAdder这一类的性能比AtomicLong要高。

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Consumer;
import java.util.function.Supplier;

public class TestAccumulator {
    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);
    }

    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());
        }
    }
}

性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。另外在设置累加单元时需要注意缓存行伪共享的问题。

java 复制代码
    // 累加单元数组, 懒惰初始化
    // transient 表示这个域不会被序列化
    // 在并发编程中,序列化包含当前运行状态的字段可能会导致问题,比如反序列化后状态与预期不符。
    // 例如,cells数组中的Cell对象保存着临时的累加值,这些值在不同的时间点可能完全不同。将这种类型的数据序列化并在其他时间或环境中恢复,很可能会失去其初衷和准确性。
    transient volatile Cell[] cells;
    // 基础值, 如果没有竞争, 则用 cas 累加这个域
    transient volatile long base;
    // 在 cells 创建或扩容时, 置为 1, 表示加锁
    transient volatile int cellsBusy;

防止缓存行伪共享

java 复制代码
    // 该注解是为了防止缓存行的伪共享
    @sun.misc.Contended
    static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }

        // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值
        final boolean cas(long prev, long next) {
            return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
        }
        // 省略不重要代码
    }

缓存行
因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。
而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte ( 8 个 long )
缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效

伪共享

如果多个CPU读取的是同一个缓存行,那么一个CPU更新了这里面的某个值(并同步到内存),另外一个CPU还得从内存中读取。这种现象就是伪共享。如下图:

因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

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

无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效

加入了@sun.misc.Contended就能防止这种现象

相关推荐
Yaml41 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~1 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616882 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7892 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java2 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~3 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
程序媛小果3 小时前
基于java+SpringBoot+Vue的旅游管理系统设计与实现
java·vue.js·spring boot
小屁孩大帅-杨一凡3 小时前
java后端请求想接收多个对象入参的数据
java·开发语言
java1234_小锋4 小时前
使用 RabbitMQ 有什么好处?
java·开发语言
TangKenny4 小时前
计算网络信号
java·算法·华为