Java并发编程-CAS

CAS、CAS自旋、CAS自旋锁、CLH锁与Java AQS:深入理解并发编程核心机制 CAS(Compare and Swap)是一种无锁(Lock-Free)的原子操作,用于在多线程环境下实现数据同步。CLH(Craig, Landin, Hagersten)锁是一种基于链表的队列锁,通过隐式队列管理线程,减少缓存一致性流量。基于CAS自旋实现的锁,线程在获取锁失败时不会阻塞,而是循环尝试。理解这些机制的原理与实现,是编写高效、健壮并发代码的关键。CAS自旋是指线程在CAS失败后循环重试,直到成功。)的核心框架,用于构建锁和同步器(如。AQS提供需子类实现的抽象方法(如。 阅读详情

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、问题引出 - 共享资源保护问题

1.1 线程安全案例

现我们假设一个账户上的余额有10000元,使用1000个线程来模拟取款,每个线程取出的金额为10元。看看1000个线程执行完毕后账户的余额情况是否为0。
编写账户接口类Account:

public interface Account {

    /**
     * 获得账户余额
     * @return
     */
    Integer getBalance();

    /**
     * 取款
     * @param amount 取出的金额
     */
    void withDraw(Integer amount);

    /**
     * 模拟1000个线程取款的情况
     * @param account
     */
    static void demo(Account account) {
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            Thread thread = new Thread(() -> {
                account.withDraw(10);
            }, "取款线程-" + (i + 1));
            threads.add(thread);
        }

        long start = System.currentTimeMillis();
        threads.forEach(Thread::start);
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.currentTimeMillis();
        System.out.println("账户余额: " + account.getBalance() + ", 执行时间: " + (end - start) / 1000);
    }
}

创建模拟存在线程安全问题的AccountUnsafe类:

public class AccountUnsafe implements Account {
    private Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    /**
     * 获得账户余额
     *
     * @return
     */
    @Override
    public Integer getBalance() {
        return balance;
    }

    /**
     * 取款
     *
     * @param amount 取出的金额
     */
    @Override
    public void withDraw(Integer amount) {
        this.balance -= amount;
        System.out.println(Thread.currentThread().getName() + "取款10元...");
    }
}

编写测试类如下:

public class UnsafeTest {
    public static void main(String[] args) {
        Account account = new AccountUnsafe(10000);
        Account.demo(account);
    }
}

运行结果如下图所示(每次运行得到的结果可能都不一样):
在这里插入图片描述
在这里插入图片描述
从上面结果可以看出,多个线程对于账户中的共享变量balance的读写交错执行引发了线程安全问题,导致最终结果并不是期望的0。

1.2 synchronized解决线程安全问题

现在我们使用加锁的方式即synchronized关键字来解决上面取款案例的线程安全问题,对于共享变量balance的读操作和写操作进行一个加锁保护。编写新的账户类AccountSyncSafe如下:

public class AccountSyncSafe implements Account {
    private Integer balance;
    
    /**
     * 获得账户余额
     *
     * @return
     */
    @Override
    public Integer getBalance() {
        synchronized (this) {
            return balance;
        }
    }

    /**
     * 取款
     *
     * @param amount 取出的金额
     */
    @Override
    public void withDraw(Integer amount) {
        synchronized (this) {
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + "取款10元...");
        }
    }
}

编写新的测试类如下:

public class SyncSafeTest {
    public static void main(String[] args) {
        Account account = new AccountSyncSafe(10000);
        Account.demo(account);
    }
}

运行结果如下所示:
在这里插入图片描述
可以看到账户余额可以正确地显示为0,即成功解决了共享资源的线程安全问题。sychronized关键字是使用了加锁的方式来解决线程安全问题的,那么有没有不使用锁也能解决线程安全问题呢?这就要使用到Java并发编程中的CAS知识。

1.3 CAS解决线程安全问题

现在我们使用CAS来解决上述取款案例中的线程安全问题,CAS中提供了各种原子类(后续文章内容会介绍),本次我们使用的是原子整数类AtomicInteger。编写新的账户类如下:

public class AccountAtomicSafe implements Account {
    private AtomicInteger balance;

    public AccountAtomicSafe(Integer balance) {
        this.balance = new AtomicInteger(balance);
    }

    /**
     * 获得账户余额
     *
     * @return
     */
    @Override
    public Integer getBalance() {
        return balance.get();
    }

    /**
     * 取款
     *
     * @param amount 取出的金额
     */
    @Override
    public void withDraw(Integer amount) {
        while (true) {
            //当前余额
            int prev = balance.get();
            //取款后需要设置的余额
            int next = prev - amount;
            //执行修改--修改成功则退出
            if (balance.compareAndSet(prev, next)) {
                System.out.println(Thread.currentThread().getName() + ": 取款10元...");
                break;
            }
        }
    }
}

编写测试类如下:
在这里插入图片描述
可以看到,使用无锁的CAS方式也能解决共享变量的线程安全问题。

二、CAS

2.1 概念

Java中的CAS(Compared-And-Set或Compare-And-Swap),是一种无锁同步技术,常用于原子操作(CPU指令级别的原子操作,不可分割),其本质就是一个原子指令,例如,在X86架构中,CAS底层对应的是lock cmpxchg指令,在单核CPU和多核CPU下都能保证【比较-交换】操作的原子性。
CAS包含三个操作数:

  1. 内存位置(V)- 表示需要进行操作的变量的位置。
  2. 预期值(A)- 表示在该内存位置的预期值。
  3. 新值(B)- 表示需要写入该内存位置的新值。

CAS操作执行步骤如下:

  1. 比较内存位置V处的值与预期值A是否相等。
  2. 如果相等,则将内存位置V处的值更新为B。
  3. 如果不相等,则不做任何操作。

整个执行过程不可被中断,在多线程情况下,只有一个线程可以成功更新值,其他线程失败并重新尝试。

2.2 案例回顾

2.2.1 分析

在1.3章节我们使用CAS对取款案例进行了一个改造,其中取款并更新余额的代码如下:

public void withDraw(Integer amount) {
        while (true) {
            //当前余额
            int prev = balance.get();
            //取款后需要设置的余额
            int next = prev - amount;
            //执行修改--修改成功则退出
            if (balance.compareAndSet(prev, next)) {
                System.out.println(Thread.currentThread().getName() + ": 取款10元...");
                break;
            }
        }
    }

整个逻辑包含在一个while循环中,如果当前线程更新余额成功,则退出该循环,否则需要不断地失败重试,更新余额时的核心逻辑compareAndSet。该过程简要示意图可如下:
在这里插入图片描述
可以看出,CAS有两个核心要素:比较-设值、失败重试。

2.2.2 CAS和valotile

回顾代码逻辑:

public void withDraw(Integer amount) {
        while (true) {
            //当前余额
            int prev = balance.get();
            //取款后需要设置的余额
            int next = prev - amount;
            //执行修改--修改成功则退出
            if (balance.compareAndSet(prev, next)) {
                System.out.println(Thread.currentThread().getName() + ": 取款10元...");
                break;
            }
        }
    }

在方法withDraw中,prev和next是两个局部变量,存储在每个线程的工作内存中,balance是共享变量,存储在主存中【主存是所有线程共享的内存区域,用于存储程序中所有的变量(包括实例变量和静态变量)】。线程工作内存区域是线程私有的内存区域,对变量的读写要在工作内存中完成,并不能直接操作主存,且线程操作的是变量的工作内存副本,所以在多线程环境下一个线程对共享变量的修改对于其他线程不可见。那么CAS又是如何确保了共享变量的修改对所有线程可见呢?
我们可以看看案例中使用的原子整数类ActomicInteger类的源码:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;
    ...

可以看到,对于成员变量value使用了volatile 修饰,即可以把value的最新值更新到主存,避免了线程从自己的工作缓存中查找变量的值,当线程需要操作共享变量时,都是到主存中获取对应的值,这就使得一个线程对共享变量的修改,对于其他线程是可见的。CAS只有借助volatile 才能读取共享变量的最新值来实现比较-交换。

三、CAS提供的原子类

Java提供的原子类在java.util.concurrent.atomic包下:
在这里插入图片描述

3.1 原子整数

我们可以使用原子整数AtomicInteger来看一下一些常用API。

3.1.1 基础运算

public class AtomicIntegerDemo {
    public static void main(String[] args) {
        AtomicInteger num = new AtomicInteger(0);
        //先自增 再获取值 => 1
        int num1 = num.incrementAndGet();
        System.out.println("num1: " + num1);
        //先获取值 再自增 => 输出1 值变为2
        int num2 = num.getAndIncrement();
        System.out.println("num2: " + num2);
        //按指定大小做增减 => 输出2 值变为12
        int num3 = num.getAndAdd(10);
        System.out.println("num3: " + num3);
        //22
        int num4 = num.addAndGet(10);
        System.out.println("num4: " + num4);
    }
}

在这里插入图片描述
回顾上述线程安全问题案例:

public void withDraw(Integer amount) {
        while (true) {
            //当前余额
            int prev = balance.get();
            //取款后需要设置的余额
            int next = prev - amount;
            //执行修改--修改成功则退出
            if (balance.compareAndSet(prev, next)) {
                System.out.println(Thread.currentThread().getName() + ": 取款10元...");
                break;
            }
        }
    }

现在可以直接调用API来替换代码逻辑:

public void withDraw(Integer amount) {
        balance.getAndAdd(-1 * amount);
    }

运行查看结果发现最终余额显示正常:
在这里插入图片描述

3.1.2 复杂运算

对一些稍微复杂的运算,可以调用API如下:

public final int updateAndGet(IntUnaryOperator updateFunction) {
        int prev, next;
        do {
            prev = get();
            next = updateFunction.applyAsInt(prev);
        } while (!compareAndSet(prev, next));
        return next;
    }
@FunctionalInterface
public interface IntUnaryOperator {
...

IntUnaryOperator 是一个函数式接口。现假设有一个原子整数变量,若该数为奇数,则该数的值翻一倍,反之若该数为偶数,则该数的值变为原值的1/2。实现示例代码如下:

public class AtomicDemo {
    static int initValue = 10;

    public static void main(String[] args) {
        AtomicInteger num = new AtomicInteger(initValue);
        num.updateAndGet(value -> {
            if (value % 2 == 0) {
                value = value / 2;
            } else {
                value = value * 2;
            }
            return value;
        });
        System.out.println("num: " + num.get());
    }
}

10是偶数,所以输出结果为5。
在这里插入图片描述
测试奇数如下:

public class AtomicDemo {
    static int initValue = 13;

    public static void main(String[] args) {
        AtomicInteger num = new AtomicInteger(initValue);
        num.updateAndGet(value -> {
            if (value % 2 == 0) {
                value = value / 2;
            } else {
                value = value * 2;
            }
            return value;
        });
        System.out.println("num: " + num.get());
    }
}

输出结果为26:
在这里插入图片描述

3.2 原子引用

Java还提供了一些原子引用类:
在这里插入图片描述
要保护的数据类型不仅仅只是基本数据类型,当需要保护引用数据类型的时候,就需要使用到相关原子引用类。

3.2.1 基本使用

现在使用原子引用类AtomicReference对上述的线程安全案例做一个改造。

public interface Account {
    /**
     * 获得账户余额
     * @return
     */
    BigDecimal getBalance();

    /**
     * 取款
     * @param amount 取出的金额
     */
    void withDraw(BigDecimal amount);

    /**
     * 模拟1000个线程取款的情况
     * @param account
     */
    static void demo(Account account) {
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            Thread thread = new Thread(() -> {
                account.withDraw(BigDecimal.TEN);
            }, "取款线程-" + (i + 1));
            threads.add(thread);
        }

        long start = System.currentTimeMillis();
        threads.forEach(Thread::start);
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.currentTimeMillis();
        System.out.println("账户余额: " + account.getBalance() + ", 执行时间: " + (end - start) / 1000);
    }
}
public class AccountReferSafe implements Account {
    private AtomicReference<BigDecimal> balance;

    public AccountReferSafe(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

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

    @Override
    public void withDraw(BigDecimal amount) {
        while (true) {
            BigDecimal prev = balance.get();
            BigDecimal next = balance.get().subtract(amount);
            if (balance.compareAndSet(prev, next)) {
            	System.out.println(Thread.currentThread().getName() + ": 取款10元...");
                break;
            }
        }
    }
}

编写测试类如下:

public class AtomicReferTest {
    public static void main(String[] args) {
        Account account = new AccountReferSafe(new BigDecimal(10000));
        Account.demo(account);
    }
}

查看测试结果依旧不会出现线程安全问题:
在这里插入图片描述

3.2.2 ABA问题

在原子类的使用中,有一个非常经典的问题即ABA问题。ABA问题是指在多线程环境下,一个线程在检查一个变量的值,并决定是否更新这个变量时,该变量的值在此期间被其他线程修改了,但最后又改回原来的值。尽管变量的值看起来没有变化,实际上它经历了改变和恢复的过程。这种情况会导致逻辑上的错误。例如:原子变量num初始值为1,线程T1读取变量num的值,发现是1,然后准备将其值改为2。在这期间,线程T2也在操作变量A,先将其值改为3,然后又将其改回1。当线程T1再次检查变量A的值时,它仍然是1,线程T1错误地认为变量的值没有被其他线程修改过,于是将变量A的值从1改为2。

3.2.2.1 案例

编写ABA问题案例如下:

public class AtomicABADemo {
    static AtomicReference<String> var = new AtomicReference<>("A");

    public static void main(String[] args) {
        try {
            log.debug(" main线程...");
            //获取变量值
            String prev = var.get();
            log.debug("var: {}", prev);
            //模拟其他线程的修改操作
            otherThreadUpdate();
            TimeUnit.SECONDS.sleep(2);
            //将A修改为C
            log.debug("main: A -> C => {}", var.compareAndSet(prev, "C"));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void otherThreadUpdate() {
        new Thread(() -> {
            log.debug("other1: A -> B => {}", var.compareAndSet(var.get(), "B"));
        }, "other1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            log.debug("other2: B -> A => {}", var.compareAndSet(var.get(), "A"));
        }, "other2").start();
    }
}

运行结果如下:
在这里插入图片描述
主线程main感知不到其他线程已经对变量var做了修改。

3.2.2.2 解决方案1 - AtomicStampedReference

ABA问题的解决方案是:加上版本号来表示每次的更新操作。例如,每次更新变量时,不仅更新变量的值,还更新其版本号。这样,即使变量的值看起来没有变化,但版本号的变化可以帮助检测到实际的变化。
Java提供了AtomicStampedReference来处理ABA问题,它将版本号(stamp)与引用(reference)捆绑在一起,以便检测ABA问题。

public class AtomicStampedReference<V> {

    private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
    ...

使用AtomicStampedReference对上述的ABA问题案例改造如下:

public class ABASolveDemo {
    /**
     * 初始变量值:A; 版本号: 0
     */
    static AtomicStampedReference<String> var = new AtomicStampedReference<>("A", 0);

    public static void main(String[] args) {
        try {
            log.debug(" main线程...");
            //获取变量值
            String prev = var.getReference();
            //获取版本号
            int stamp = var.getStamp();
            log.debug("变量值: {}, 版本号: {}", prev, stamp);
            //模拟其他线程的修改操作
            otherThreadUpdate();
            TimeUnit.SECONDS.sleep(2);
            //将A修改为C
            log.debug("当前版本号: {}", var.getStamp());
            log.debug("main: A -> C => {}", var.compareAndSet(prev, "C", stamp, stamp + 1));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void otherThreadUpdate() {
        new Thread(() -> {
            int stamp = var.getStamp();
            log.debug("当前版本号: {}", stamp);
            //更新成功: 版本号 + 1
            log.debug("other1: A -> B => {}", var.compareAndSet(var.getReference(), "B",
                    stamp, stamp + 1));
        }, "other1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            int stamp = var.getStamp();
            log.debug("当前版本号: {}", stamp);
            log.debug("other2: B -> A => {}", var.compareAndSet(var.getReference(), "A",
                    stamp, stamp + 1));
        }, "other2").start();
    }
}

运行结果如下:
在这里插入图片描述
main线程并不能将变量值从A修改为C,因为其他线程对变量做了修改,版本号已经是2而不再是0。

3.2.2.3 解决方案2 - AtomicMarkableReference

AtomicStampedReference通过一个整数标记-版本号来追踪原子引用的整个过程,每次引用更新,版本号也跟着更新。但是如果我们并不想过于关注引用值变化了几次,只想关注引用是否更改过,这是就需要使用AtomicMarkableReference。

public class AtomicMarkableReference<V> {

    private static class Pair<T> {
        final T reference;
        final boolean mark;
        private Pair(T reference, boolean mark) {
            this.reference = reference;
            this.mark = mark;
        }
        static <T> Pair<T> of(T reference, boolean mark) {
            return new Pair<T>(reference, mark);
        }
    }

    private volatile Pair<V> pair;
    ...

AtomicMarkableReference维护了一个引用和一个布尔标记(mark),并通过检查和设置这些值来确保原子操作的正确性。
假设一个场景:在一栋房屋中,垃圾袋(garbagebag)满了以后,如果保洁员(housekeeper)有空那么保洁员就换上一个新垃圾袋,如果主人(main)有空那么主人就换上一个新垃圾袋。如果已经换上了新垃圾袋,那么就不需要再换上新垃圾袋。
编写垃圾袋类如下:

public class GarbageBag {
    private String desc;

    public GarbageBag(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    @Override
    public String toString() {
        return "GarbageBag{" +
                "desc='" + desc + '\'' +
                '}';
    }
}

测试类如下[假设初始场景垃圾袋就已经满了]:

public class ABASolveDemo2 {
    public static void main(String[] args) {
        GarbageBag bag = new GarbageBag("装满了垃圾");
        AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
        log.debug("main...");
        GarbageBag prev = ref.getReference();
        log.debug("垃圾袋: {}", prev.toString());
        //保洁员
        new Thread(() -> {
            log.debug("保洁员...");
            //垃圾袋已满,需要换上新垃圾袋
            bag.setDesc("新垃圾袋");
            if (ref.compareAndSet(bag, bag, true, false)) {
                log.debug("换了一只新垃圾袋...");
            }
        }, "housekeeper").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("需要换一只新垃圾袋吗?");
        boolean isSuccess = ref.compareAndSet(bag, new GarbageBag("新垃圾袋"), true, false);
        log.debug("是否换了新垃圾袋: {}", isSuccess);
        log.debug("垃圾袋: {}", bag.toString());
    }
}

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

3.3 原子数组

3.3.1 问题演示

现模拟一个多线程下普通数组存在线程安全场景。假设有一个长度为10的整数数组,初始值都是0,现有10个[数组长度]线程对数组的每个元素操作10000次自增加1操作。代码如下:

public class ArrayUnSafe {
    /**
     * 数组测试案例
     * @param arraySupplier 提供数组
     * @param lengthFun 获得数组长度
     * @param putConsumer 自增方法 => 返回值(array index)
     * @param printConsumer 打印数组
     * @param <T>
     */
    public static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lengthFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer
    ) {
        List<Thread> threads = new ArrayList<>();
        T array = arraySupplier.get();
        Integer arrLength = lengthFun.apply(array);
        for (int i = 0; i < arrLength; i++) {
            //每个线程对每个元素操作10000次:自增+1
            threads.add(
                    new Thread(() -> {
                        for (int j = 0; j < 10000; j++) {
                            //回传array index => 获得数组对应下标元素
                            putConsumer.accept(array, j % arrLength);
                        }
                    }, "t" + (i + 1))
            );
        }

        //启动所有线程
        threads.forEach(Thread::start);
        //等待所有线程执行完毕
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        //打印查看数组
        printConsumer.accept(array);
    }
}

测试类如下:

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

查看运行结果:
在这里插入图片描述
多次运行,发现结果并不是想象中的[10000,10000,10000,10000,10000,10000,10000,10000,10000,10000]。

3.3.2 问题解决

在上面数组元素累加案例中,由于多线程下读写交错执行导致出现线程安全问题,最终输出结果和预期结果并不相同。现在使用原子数组AtomicIntegerArray来改造上述案例,如下:

public class ArraySafeTest {
    public static void main(String[] args) {
        System.out.println("普通数组...");
        ArrayUnSafe.demo(
                () -> new int[10],
                (array) -> array.length,
                (array, index) -> array[index]++,
                array -> System.out.println(Arrays.toString(array))
        );
        System.out.println("原子数组...");
        ArrayUnSafe.demo(
                () -> new AtomicIntegerArray(10),
                (array) -> array.length(),
                (array, index) -> array.getAndIncrement(index),
                array -> System.out.println(array)
        );
    }
}

输出结果如下:
在这里插入图片描述
可以看到,使用原子数组解决了线程安全问题。

3.4 原子更新器

Java还提供了字段更新器来专门针对对象的某个域进行原子操作,需要配合valotile修饰,且访问控制符不能是private,否则将会抛出异常。示例如下:

public class Worker {
    volatile String name;

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

测试类如下(假设当前只有主线程在执行):

public class CasUpdateDemo {
    public static void main(String[] args) {
        Worker worker = new Worker();
        AtomicReferenceFieldUpdater<Worker, String> ref = AtomicReferenceFieldUpdater
                .newUpdater(Worker.class, String.class, "name");
        //如果当前为null 则修改为qmc
        log.debug("修改成功: {}", ref.compareAndSet(worker, null, "qmc"));
        System.out.println(worker);
    }
}

运行结果:
在这里插入图片描述
现假设还有其他线程同样尝试修改name属性值:

public class CasUpdateDemo {
    static Random random = new Random();

    public static void main(String[] args) {
        Worker worker = new Worker();
        AtomicReferenceFieldUpdater<Worker, String> ref = AtomicReferenceFieldUpdater
                .newUpdater(Worker.class, String.class, "name");
        new Thread(() -> {
            try {
                //随机休眠
                int time = random.nextInt(3) + 1;
                log.debug("休眠时间: {}", time);
                TimeUnit.SECONDS.sleep(time);
                log.debug("是否修改成功: {}", ref.compareAndSet(worker, null, "qmc"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        //如果当前为null 则修改为qmc
        //随机休眠
        try {
            int time = random.nextInt(3) + 1;
            log.debug("休眠时间: {}", time);
            TimeUnit.SECONDS.sleep(time);
            log.debug("是否修改成功: {}", ref.compareAndSet(worker, null, "qmc"));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(worker);
    }
}

运行结果如下:
在这里插入图片描述
如果其他线程已经修改了对应属性值,则当前线程执行修改会失败。

3.5 原子累加器

Java还提供了原子累加器来专门做累加计算,其性能比单独使用原子整数等做累加的效率要高。示例代码如下:

public class CasAdderDemo {
    public static void main(String[] args) {
        log.info("普通原子长整型做累加计算...");
        for (int i = 0; i < 5; i++) {
            demo(
                    () -> new AtomicLong(0),
                    (adder) -> adder.getAndIncrement()
            );
        }

        log.info("原子累加器做累加计算...");
        for (int i = 0; i < 5; i++) {
            demo(
                    () -> new LongAdder(),
                    (adder) -> adder.increment()
            );
        }
    }

    /**
     * 测试样例方法
     * @param adderSupplier 累加计算的原子类或者原子累加器
     * @param action 累加操作
     * @param <T>
     */
    private static <T> void demo(
            Supplier<T> adderSupplier,
            Consumer<T> action
    ) {
        T adder = adderSupplier.get();
        List<Thread> threads = new ArrayList<>();
        //4个线程:每个线程累加500000次
        for (int i = 0; i < 4; i++) {
            threads.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }, "adder" + (i + 1)));
        }

        long start = System.currentTimeMillis();
        //启动
        threads.forEach(Thread::start);
        //等待执行完毕
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.currentTimeMillis();
        log.debug("本次累加计算耗时: {}ms", (end - start));
    }
}

运行结果如下所示:
在这里插入图片描述
可以看到,使用累加器计算的速度能比单独使用原子类计算大概快几倍左右。

四、UnSafe对象

4.1 概念

在Java中,UnSafe类是一个比较偏底层的类,一般来说较少会直接操作这个类的对象(光是看名字都有点吓人了),UnSafe对象提供了非常底层的,操作内存、线程的方法,允许开发者直接操作内存和线程,从而绕过Java虚拟机的一些安全机制,如果使用不当出现错误可能导致程序崩溃、数据损坏甚至安全漏洞。

4.2 获取

Unsafe类是JDK内部使用的,无法通过常规的方式直接获取,只能通过反射的方式来获取,大致看一下UnSafe源码如下:

public final class Unsafe {
    private static final Unsafe theUnsafe;
    public static final int INVALID_FIELD_OFFSET = -1;
    public static final int ARRAY_BOOLEAN_BASE_OFFSET;
    ...

theUnsafe私有且是单例。可以通过反射获取Filed来获取UnSafe对象,代码如下:

public class GetUnSafeDemo {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        //反射爆破
        theUnsafe.setAccessible(true);
        //获取
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        System.out.println("unsafe: " + unsafe);
    }
}

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

4.3 使用示例

4.3.1 CAS方式操作对象属性

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

通过UnSafe对象给Student对象属性赋值如下:

public class UnsafeUseDemo1 {
    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        //反射爆破
        theUnsafe.setAccessible(true);
        //获取
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        //获得属性域偏移地址
        long idOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
        long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));

        Student student = new Student();
        //cas操作 => param: 实例对象 属性域偏移值 原始值 修改值
        unsafe.compareAndSwapInt(student, idOffset, 0, 18);
        unsafe.compareAndSwapObject(student, nameOffset, null, "qmc");

        System.out.println(student);
    }
}

在这里插入图片描述

4.3.2 直接内存操作

public class UnsafeUseDemo2 {
    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        //反射爆破
        theUnsafe.setAccessible(true);
        //获取
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        //分配1kb的内存
        long memory = unsafe.allocateMemory(1024);
        //初始化内存
        unsafe.setMemory(memory, 1024, (byte) 0);
        //释放内存
        unsafe.freeMemory(memory);
    }
}

4.3.3 数组操作

public class UnsafeUseDemo3 {
    public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        //反射爆破
        theUnsafe.setAccessible(true);
        //获取
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        int[] array = new int[10];
        //数组基地址
        int baseOffset = unsafe.arrayBaseOffset(int[].class);
        //元素偏移量
        int indexScale = unsafe.arrayIndexScale(int[].class);
        //设置第6个元素为42
        unsafe.putInt(array, baseOffset + indexScale * 5, 42);
        //获取第6个元素的值
        int sixVal = unsafe.getInt(array, baseOffset + indexScale * 5);
        System.out.println("第6个元素的值: " + sixVal);
    }
}

在这里插入图片描述

4.4.4 模拟实现原子整数

使用UnSafe对象对本文开头的取款案例进行一个改造。定义一个获取UnSafe对象的类MyUnSafeAccessor :

public class MyUnSafeAccessor {
    private static final Unsafe unsafe;

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            //转为运行时异常抛出
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    public static Unsafe getUnsafe() {
        return unsafe;
    }
    
}

账户接口:

public interface Account {
    /**
     * 获得账户余额
     * @return
     */
    Integer getBalance();

    /**
     * 取款
     * @param amount 取出的金额
     */
    void withDraw(Integer amount);

    /**
     * 模拟1000个线程取款的情况
     * @param account
     */
    static void demo(com.zkt.article.demo1.Account account) {
        List<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            Thread thread = new Thread(() -> {
                account.withDraw(10);
            }, "取款线程-" + (i + 1));
            threads.add(thread);
        }

        long start = System.currentTimeMillis();
        threads.forEach(Thread::start);
        threads.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.currentTimeMillis();
        System.out.println("账户余额: " + account.getBalance() + ", 执行时间: " + (end - start) / 1000);
    }
}

账户实现类:

public class MyAccount implements Account {
    /**
     * Unsafe对象
     */
    private static final Unsafe unsafe;
    /**
     * 属性偏移量
     */
    private static final long valueOffset;
    /**
     * 属性值 -- volatile 保证可见性
     */
    private volatile int balance;

    static {
        unsafe = MyUnSafeAccessor.getUnsafe();
        try {
            valueOffset = unsafe.objectFieldOffset(MyAccount.class.getDeclaredField("balance"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

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

    @Override
    public Integer getBalance() {
        return balance;
    }

    @Override
    public void withDraw(Integer amount) {
        while (true) {
            int prev = balance;
            int next = balance - amount;
            if (unsafe.compareAndSwapInt(this, valueOffset, prev, next)) {
                log.info("取款: {}元成功", amount);
                break;
            }
        }
    }
}

编写测试类如下:

public class MyTest {
    public static void main(String[] args) {
        Account account = new MyAccount(10000);
        Account.demo(account);
    }
}

测试结果如下:
在这里插入图片描述
可以看到,通过UnSafe对象实现的CAS操作也能有效防止线程安全问题。


总结

CAS可以实现无锁并发,适合于线程数较少、多核CPU的场景下。CAS基于乐观锁的设计思想,即最乐观地估计不会发生线程安全问题,等出现线程安全问题时执行重试操作。CAS体现的是无锁并发、无阻塞并发,因为没有使用synchronized,所以线程并不会进入阻塞状态。但是如果线程之间的竞争十分激烈,则重试必然要频繁地发生,那么效率就会收到影响。本次关于CAS部分的知识就分享到这了。

Java 的并发编程模型:从 synchronized 到 CAS 和CAS是Java并发编程模型中两种重要的机制。简单易用,能够保证线程安全,但在高并发场景下性能较低。CAS通过无锁的方式实现了高效的并发控制,但在某些情况下可能出现ABA问题。在实际开发中,我们需要根据具体的应用场景选择合适的并发控制机制,以提高程序的性能和可靠性。 阅读详情

相关推荐

USB的24脚和12脚TYPEC引脚定义

USB 是一种接口,用于将设备连接到计算机上。通过该连接,计算机可以向设备发送数据或检索来自设备的数据。USB 为开发者提供了一个标准接口,适用于多种不同类型的应用。借助于系统的设计过程,USB 设备易于连接和使用。本应用笔记会使该过程变得更加简单。以下是本应用笔记中会使用的概念:USB 历史USB 架构USB 物理接口USB 速度USB 电源USB 端点USB 通信协议USB 描述符USB 类设备USB 枚举与配置过程。

易板 1万+

JAVA并发编程: CAS和AQS

说起JAVA并发编程,就不得不聊聊CAS(Compare And Swap)和AQS了(AbstractQueuedSynchronizer)。 CAS(Compare And Swap) 什么是CAS CAS(Compare And Swap),即比较并交换。是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新

Vioao's Blog 4万+

深入解析AutoConfigURL劫持:从注册表权限到一键修复

本文深入解析了AutoConfigURL劫持的原理,揭示其通过篡改注册表权限锁定浏览器设置的机制。文章提供了从手动修复注册表权限到使用一键修复工具的详细解决方案,并指导用户进行DNS检查与病毒查杀,以彻底根除此类顽固劫持问题。

weixin_29229989的博客 770

Java CAS并发编程解析

在当今的互联网时代,高并发应用的开发已经成为一项基本技能。以一个在线购物平台为例,当用户在高峰时段进行购物时,系统可能会面临大量的并发请求。在这样的场景下,如果数据更新操作没有妥善处理,就很容易出现数据不一致的问题。为了解决这个问题,我们需要介绍Java高并发知识点之Compare-And-Swap(CAS)。在多线程环境中,当多个线程尝试同时修改同一数据时,传统的锁机制可能会导致性能瓶颈。

我是Java程序员廖志伟,感谢朋友们的支持!不定期贡献一篇高品质、过万文字、图文并茂且附有视频解说、满载代码示例注释的良心之作,坚决杜绝粗制乱造。 829

LibreNMS实战:5分钟搞定Docker部署与中文配置(附常见问题排查)

本文详细介绍了如何使用Docker在5分钟内快速部署LibreNMS,并完成中文界面优化。从环境准备到容器启动,再到中文语言包安装和系统配置调整,提供了完整的实战指南。同时,文章还包含常见问题排查和性能调优建议,帮助用户高效搭建网络监控系统。

e1f2g的博客 1282

OpenHarmony自定义Launcher

OpenHarmony源码版本:4.0release开发板:DAYU / rk3568DevEco Studio版本:4.0.0.600切换分支为OpenHarmony-4.0-Release,并下载源码。

Abner_Crazy的博客 1630

java并发编程 Atomic、CAS和AQS的介绍

/ 自定义信号量类 class CustomSemaphore {// 构造函数,初始化信号量许可数量 public CustomSemaphore(int permits) {} // 获取许可 public void acquire() throws InterruptedException {} // 释放许可 public void release() {

不知道说什么的博客 1101

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的远距离停车位检测系统(深度学习代码+UI界面+训练数据集)

本文详细介绍了基于YOLOv5/YOLOv6/YOLOv7/YOLOv8深度学习模型的远距离停车位检测系统的完整实现方案。该系统能够准确识别200米范围内的停车位状态,为智能停车场管理系统、自动驾驶车辆提供关键的视觉感知能力。文章涵盖算法原理、数据集构建、模型训练、系统实现及优化策略,并提供完整的代码实现和UI界面设计。关键词:YOLO系列,深度学习,停车位检测,计算机视觉,智能交通系统pythonimport cv2import os"""创建数据存储目录""""""定时采集图像"""

YOLO 186

java cas求和_Java并发编程之CAS

在Java并发编程的世界里,synchronized 和 Lock 是控制多线程并发环境下对共享资源同步访问的两大手段。其中 Lock 是 JDK 层面的锁机制,是轻量级锁,底层使用大量的自旋+CAS操作实现的。那什么是CAS呢?CAS,compare and swap,即比较并交换,什么是比较并交换呢?在Lock锁的理念中,采用的是一种乐观锁的形式,即多线程去修改共享资源时,不是在修改之前就加锁...

weixin_29346059的博客 117

大模型工作流Dify面试题及参考答案

设计一个基于 Dify 的智能摘要与重点提取 Agent,需结合Prompt 工程、模型能力配置和工作流优化,确保对长文本的高效解析和关键信息提炼。1. 明确任务需求与输入输出输入:长文本内容(如文章、报告、会议记录)或文本 URL输出摘要:简洁概括原文核心内容,控制在原文长度的 10%-20%重点列表:分点列出关键数据、结论、行动项等可选:关键词提取、情感倾向分析示例场景输入:一篇 5000 字的行业报告。

大模型大数据攻城狮的专栏 2519

thinkphp6/8 - 实现评论区发表评论、回复评论功能数据库表设计与整个逻辑思路过程分析,tp5 评论盖楼等功能(附带源码稍加改造即可使用)

tp5评论区数据库表设计,tp5发表回复评论功能设计,thinkphp5如何做评论功能,tp5 php评论区实现发表回复删除评论,tp5评论区盖楼源码及数据表设计思路,thinkphp评论功能详细教程,thinkphp实现评论与回复功能,thinkphp实现文章评论和回复评论功能,tp5实现评论回复功能,thinkphp如何实现评论区,tp5网站PC移动端h5怎么做评论区发表留言及回复功能,thinkphp实现评论区插件,thinkphp评论区相互发表回复功能详细教程,thinkphp有没有评论区的组件插件

✅ 精选推荐文章 3160

java cas并发_Java并发编程之CAS

在Java并发编程的世界里,synchronized 和 Lock 是控制多线程并发环境下对共享资源同步访问的两大手段。其中 Lock 是 JDK 层面的锁机制,是轻量级锁,底层使用大量的自旋+CAS操作实现的。学习并发推荐《Java并发编程的艺术》那什么是CAS呢?CAS,compare and swap,即比较并交换,什么是比较并交换呢?在Lock锁的理念中,采用的是一种乐观锁的形式,即多线程...

weixin_33694136的博客 114

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

Java并发编程之CAS第一篇理解

Java并发编程之CAS第一篇-什么是CAS 通过前面几篇的学习,我们对并发编程两个高频知识点了解了其中的一个—volatitl。从这一篇文章开始,我们将要学习另一个知识点—CAS.本篇是《凯哥并发编程学习》系列之《CAS系列》教程的第一篇:什么是CAS。 本文主要内容: 生活中举例;CAS定义;CAS代码演示。 一:生活中的例子 在电影《智取威虎山》中,杨子荣进入威虎山的时候,土匪说:天王盖地虎,杨子荣对:宝塔镇河妖。类似这样的黑话,几个土匪和杨子荣对过之后,座山雕,三爷才发话:这么说,你是徐旅

115

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Java并发编程之CAS

在进行Java并发编程的时候,我们都会使用到锁来控制并发线程对临界资源的安全访问。用的最多的就是synchronized,但是synchronized属于重量级锁、悲观锁,在很多时候会引起性能问题。除了synchronized还有cas,cas属于乐观锁,在一定的场景下会比synchronized更高效。java.util.concurrent包下的ReentrantLock内部的AQS,还...

码行天下 251

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Java 并发编程的 CAS(Compare and Swap)是什么?

并发库的“心脏”。

qq_74825504的博客 1049

Java并发编程 - CAS 底层剖析

Java并发编程 - CAS 底层剖析 由volatile关键字可知,他可以保证JMM内存模型的可见性和有序性(禁止指令重排序)但是不能保证原子性,导致线程安全问题,我们可以加synchronized关键字来解决但是对于有些业务来说,synchronized太重了开销太大,所以我们可以用JUC包下的原子操作系列Atomic操作类,提供了操作的原子性。在底层的实现原理还是CAS。 CAS:...

J-Proton博客 371

Java并发编程:CAS操作

CAS(Compare-and-Swap)是一种原子操作,用于在多线程环境中实现无锁同步。它通过比较内存地址的当前值与预期值,如果匹配则更新为新值,否则不执行操作。CAS的优势在于减少锁的使用,提升并发性能,但其局限性包括只能操作单一内存地址、设计复杂、可能引发ABA问题。ABA问题发生在变量值经历A->B->A变化时,导致冲突检查失效。为解决ABA问题,可以使用带有版本戳的原子引用类,如AtomicStampedReference,但这会增加存储和性能开销。

qj19842011的专栏 822

Java并发编程之CAS算法

前言 在谈CAS算法时,我们先来了解一下无锁的概念。 无锁分为以下两大派系: 对于乐观派系而言,它们认为事情总会往好的方向去发展,总是认为坏的情况发生概率特别小,可以无所顾忌的做任何事情; 对于悲观派系而言,它们总会认为发展事态如果不及时控制,以后就无法挽回,即时此种局面不会发生的情况下。 上述两大派系映射到并发编程中就如同加锁与无锁策略,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁...

q309572960的博客 224

【java并发编程】cas乐观锁

一、cas是什么? cas全程Compare and swap,比较并交换。一种无锁原子算法,是一种乐观锁,用于原子性的更新变量。 二、cas实现原理 CAS靠硬件实现,是一条CPU的原子指令,基于汇编指令cmpxchg(Intel x86)实现,其作用是让CPU先比较两个值是否相等,然后原子性地更新某个内存地址的值。 三、cas操作在java使用 unsafe类中的compareAndSwapInt方法 是经典实现 进入native方法,在c++源码unsafe.cpp文件中

sumengnan的博客 984

java cas机制_java并发编程中的CAS机制,你理解嘛?

学习Java并发编程,CAS机制都是一个不得不掌握的知识点。这篇文章主要是从出现的原因再到原理进行一个解析。希望对你有所帮助。一、为什么需要CAS机制?为什么需要CAS机制呢?我们先从一个错误现象谈起。我们经常使用volatile关键字修饰某一个变量,表明这个变量是全局共享的一个变量,同时具有了可见性和有序性。但是却没有原子性。比如说一个常见的操作a++。这个操作其实可以细分成三个步骤:(1)从内...

weixin_36138385的博客 289

java cas求和_java并发编程之cas详解

CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。这听起来可能有一点复杂但是实际上你理解之后发现很简单,接下来,让我们跟深入的了解一下这项技术。CAS的使用场景在程序和算法中一个经常出现的模式就是“check and act”模式。先...

weixin_39609670的博客 127

java并发编程之CAS

CAS(Compare-and-Swap):比较替换。这个思想非常的重要,在Java并发编程中多处用到,包括轻量级锁也用到了CAS的思想。所以我觉得有必要单独学习一下。 class CAS{ Object value; Object getValue(){ return this.value; } void setValue(Object...

pochengxiaowei的博客 186
上一篇: JavaSE-位运算
下一篇: Java并发编程 - ReentrantLock
Zzzkt
博客等级 码龄7年 149粉丝 6原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值