【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就能防止这种现象

相关推荐
Coder码匠34 分钟前
Dockerfile 优化实践:从 400MB 到 80MB
java·spring boot
李慕婉学姐8 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆10 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin10 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model200510 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉10 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国10 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_9418824811 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈11 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_9911 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc