前言

JDK中提供了一系列的基于CAS实现的原子类,CAS 的全称是Compare-And-Swap,底层是lock cmpxchg指令,可以在单核和多核 CPU 下都能够保证比较交换的原子性。所以说,这些原子类都是线程安全的,而且是无锁并发,线程不会频繁上下文切换,所以在某些场景下性能是优于加锁。

本文就盘点一下JDK中的原子类,方便我们后续拿来使用。

基础原子类

  • AtomicInteger:Integer整数类型的原子操作类
  • AtomicBoolean:Boolean类型的原子操作类
  • AtomicLong:Long类型的原子操作类

这边以AtomicInteger讲解下它的API和用法。

构造方法:

  • public AtomicInteger():初始化一个默认值为 0 的原子型 Integer
  • public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer

常用API:

  • public final int get(): 获取 AtomicInteger 的值
  • public final int getAndIncrement(): 以原子方式将当前值加 1,返回的是自增前的值
  • public final int incrementAndGet():以原子方式将当前值加 1,返回的是自增后的值
  • public final int getAndSet(int value):以原子方式设置为 newValue 的值,返回旧值
  • public final int addAndGet(int data):以原子方式将输入的数值与实例中的值相加并返回

使用:

  • 结果1000,大致说明并发情况下保证了线程安全

原理分析:

整体实现思路: 自旋(循环) + CAS算法

  • 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
  • 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
  • valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据

  • 从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行 compareAndSwapInt() 再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据。

  • 原子类AtomicIntegervalue属性是volatile类型,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量。

原子引用

原子引用主要是对对象的原子操作,原子引用类分为AtomicReferenceAtomicStampedReferenceAtomicMarkableReference。它们之间有什么区别呢?

  1. AtomicReference类

普通的原子类对象

public class AtomicReferenceDemo {
public static void main(String[] args) {
User user1 = new User("旭阳"); // 创建原子引用包装类
AtomicReference<User> atomicReference = new AtomicReference<>(user1); while (true) {
User user2 = new User("alvin");
// 比较并交换
if (atomicReference.compareAndSet(user1, user2)) {
break;
}
}
System.out.println(atomicReference.get());
}
} @Data
@AllArgsConstructor
@ToString
class User {
private String name;
}
  • 调用compareAndSet()方法进行比较替换对象

ABA问题

但是如果使用AtomicReference类,会有一个ABA问题。什么意思呢?就是一个线程将共享变量从A改成B, 后面又改回A, 这是,另外一个线程就无法感知这个变化过程,就傻傻的比较,就以为没有变化,还是一开始的A,就替换了。 实际的确存在这样只要共享变量发生过变化,就要CAS失败,有什么办法呢?

  1. AtomicStampedReference类

带版本号的原子类对象

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

  • 虽然对象的值变回了A,但是由于版本变了,所以主线程CAS失败
  1. AtomicMarkableReference 类

其实有时候并不关心共享变量修改了几次,而是只要标记下是否发生过更改,是否加个标记即可,所以就有了AtomicMarkableReference类。

@Slf4j(topic = "c.AtomicMarkableReferenceTest")
public class AtomicMarkableReferenceTest {
// 构造 AtomicMarkableReference, 初始标记为false
static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false); public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
other();
Thread.sleep(1000);
// 看看是否发生了变化
log.debug("change {}", ref.isMarked());
}
private static void other() throws InterruptedException {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
false, true));
}, "t1").start(); Thread.sleep(500); new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
true, true));
}, "t2").start();
}
}

  • 通过调用isMarked()方法查看是否发生变化。

原子数组

  • AtomicIntegerArray: Integer类型的原子数组
  • AtomicLongArray:Long类型的原子数组
  • AtomicReferenceArray:引用类型的原子数组

直接上例子

public class AtomicIntegerArrayTest {
public static void main(String[] args) throws Exception{
AtomicIntegerArray array = new AtomicIntegerArray(10);
Thread t1 = new Thread(()->{
int index;
for(int i=1; i<100000; i++) {
index = i%10; //范围0~9
array.incrementAndGet(index);
}
}); Thread t2 = new Thread(()->{
int index;
for(int i=1; i<100000; i++) {
index = i%10; //范围0~9
array.decrementAndGet(index);
}
});
t1.start();
t2.start();
Thread.sleep(5 * 1000);
System.out.println(array.toString());
}
}

  • 两个线程同时对数组对象进行加和减的操作,最终结果都是0,说明线程安全。

原子字段更新器

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

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

@Data
public class AtomicReferenceFieldUpdaterTest { private volatile int age = 10; private int age2; public static void main(String[] args) {
AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age"); AtomicReferenceFieldUpdaterTest ref = new AtomicReferenceFieldUpdaterTest();
// 对volatile 的age字段+1
integerFieldUpdater.getAndIncrement(ref);
System.out.println(ref.getAge()); // 修改 非volatile的age2
integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age2");
integerFieldUpdater.getAndIncrement(ref);
}
}

  • 原子字段更新器只能更新volatile字段,它可以保证可见性,但是无法保证原子性。

原子累加器

原子累加器主要是用来做累加的,相关的类有LongAdderDoubleAdderLongAccumulatorDoubleAccumulator

LongAdder是jdk1.8中引入的,它的性能要比AtomicLong方式好。

LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 提供了更强大的功能,可以自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr

这边做个性能的对比例子。

public class LongAdderTest {

    public static void main(String[] args) {
System.out.println("LongAdder ...........");
for (int i = 0; i < 5; i++) {
addFunc(() -> new LongAdder(), adder -> adder.increment());
}
System.out.println("AtomicLong ...........");
for (int i = 0; i < 5; i++) {
addFunc(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}
} private static <T> void addFunc(Supplier<T> adderSupplier, Consumer<T> action) {
T adder = adderSupplier.get();
long start = System.nanoTime();
List<Thread> ts = new ArrayList<>(); // 40个线程,每人累加 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);
}
}

主要是由于LongAdder会设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性能。

总结

本文总结了JDK中提供的各种原子类,包括基础原子类、原子引用类、原子数组类、原子字段更新器和原子累加器等。有时候,使用这些原子类的性能是比加锁要高的,特别是在读多写少的场景下。但是,不知道大家发现没有,所有的原子类操作对于一个共享变量执行操作是原子的,如果对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性,还是老老实实加锁吧。

如果本文对你有帮助的话,请留下一个赞吧

更多技术干活和学习资料尽在个人公众号——JAVA旭阳

盘点JAVA中基于CAS实现的原子类, 你知道哪些?的更多相关文章

  1. 浅析CAS与AtomicInteger原子类

    一:CAS简介 CAS:Compare And Swap(字面意思是比较与交换),JUC包中大量使用到了CAS,比如我们的atomic包下的原子类就是基于CAS来实现.区别于悲观锁synchroniz ...

  2. 沉淀再出发:java中的CAS和ABA问题整理

    沉淀再出发:java中的CAS和ABA问题整理 一.前言 在多并发程序设计之中,我们不得不面对并发.互斥.竞争.死锁.资源抢占等等问题,归根到底就是读写的问题,有了读写才有了增删改查,才有了所有的一切 ...

  3. Java中的CAS实现原理

    一.什么是CAS? 在计算机科学中,比较和交换(Conmpare And Swap)是用于实现多线程同步的原子指令. 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新 ...

  4. java 多线程12 : 无锁 实现CAS原子性操作----原子类

    由于java 多线程11:volatile关键字该文讲道可以使用不带锁的情况也就是无锁使变量变成可见,这里就理解下如何在无锁的情况对线程变量进行CAS原子性及可见性操作 我们知道,在并发的环境下,要实 ...

  5. java高并发系列 - 第21天:java中的CAS操作,java并发的基石

    这是java高并发系列第21篇文章. 本文主要内容 从网站计数器实现中一步步引出CAS操作 介绍java中的CAS及CAS可能存在的问题 悲观锁和乐观锁的一些介绍及数据库乐观锁的一个常见示例 使用ja ...

  6. java中基于TaskEngine类封装实现定时任务

    主要包括如下几个类: 文章标题:java中基于TaskEngine类封装实现定时任务 文章地址: http://blog.csdn.net/5iasp/article/details/10950529 ...

  7. Java中的CAS原理

    前言:在对AQS框架进行分析的过程中发现了很多CAS操作,因此有必要对CAS进行一个梳理,也便更清楚的了解其原理. 1.CAS是什么 CAS,是compare and swap的缩写,中文含义:比较交 ...

  8. Java并发--Java中的CAS操作和实现原理

    版权声明:本文为博主原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/CringKong/article/deta ...

  9. Conccrent中 Unsafe类原理 以及 原子类AutomicXX的原理以及对Unsafe类的使用

    Unsafe类的介绍 Java中基于操作系统级别的原子操作类sun.misc.Unsafe,它是Java中对大多数锁机制实现的最基础类.请注意,JDK 1.8和之前JDK版本的中sun.misc.Un ...

  10. Java开发知识之Java中的集合Set接口以及子类应用

    ---恢复内容开始--- Java开发知识之Java中的集合Set接口以及子类应用 一丶Set接口以及作用 在上一讲.我们熟悉了接口的实现图.以及自己各有的子类. List接口主要存储的数据是可以重复 ...

随机推荐

  1. 复现禅道V16.5的SQL注入(CNVD-2022-42853)

    漏洞详情 禅道V16.5未对输入的account参数内容作过滤校验,导致攻击者拼接恶意SQL语句执行. 环境搭建 环境下载:禅道V16.5 下载后双击运行,进入目录运行start.exe 直接访问即可 ...

  2. POJ1985 Cow Marathon (树的直径)

    用两次dfs求出树的直径,这两次dfs可以写在一起,当然为了方便理解,这里是分开写的. 1 //两次dfs求树的重心 2 #include<cstdio> 3 #include<cs ...

  3. 支持 Java 8/11/17/19 的框架,Solon v1.10.5 版本发布

    Java 轻量级应用开发框架.可用来快速开发 Java 应用项目,主框架仅 0.1 MB. 相对于 Spring Boot 和 Spring Cloud 的项目: 启动快 5 - 10 倍. (更快) ...

  4. Containerd-1.6.5 镜像容器操作

    一.Containerd 镜像操作 1 基本参数 [root@ecs-65685 ~]# ctr c NAME: ctr containers - manage containers USAGE: c ...

  5. 项目管理构建工具——Maven(高阶篇)

    项目管理构建工具--Maven(高阶篇) 我们在之前的文章中已经基本了解了Maven,但也仅仅只止步于了解 Maven作为我们项目管理构建的常用工具,具备许多功能,在这篇文章中我们来仔细介绍 分模块开 ...

  6. 基于tauri打造的HTTP API客户端工具-CyberAPI

    国庆长假和朋友聚会的时候,和朋友谈起最近这段时间捣鼓tauri,写了一个HTTP API客户端工具.『你写了这么多东西,其实有想过是为了啥不?』为了啥这是一个很大的命题,当初每个项目的时候都想过它应该 ...

  7. 20220729 - DP训练 #2

    20220729 - DP训练 #2 时间记录 \(8:00-8:10\) 浏览题面 \(8:10-8:50\) T1 看题想到了建树,从每一个点遍历,若能遍历每一个点,则可以获胜 快速写完之后,发现 ...

  8. 解决在JS中阻止定时器“重复”开启问题、Vue中定时器的使用

    1.问题描述 在一些需求开发中.需要设定软件提供服务的时间段(营业时间).这时可以选择定时器来实现.可以选择让定时器每隔一段时间检测当前时间是否在服务时间.到达服务时间.进入服务状态.未到服务时间.进 ...

  9. vue3中$attrs的变化与inheritAttrs的使用

    在vue3中的$attrs的变化 $listeners已被删除合并到$attrs中. $attrs现在包括class和style属性. 也就是说在vue3中$listeners不存在了.vue2中$l ...

  10. CF Round #829 题解 (Div. 2)

    F 没看所以摆了 . 看拜月教教主 LHQ 在群里代打恰钱 /bx 目录 A. Technical Support (*800) B. Kevin and Permutation (*800) C. ...