JUC从实战到源码:原子类全解析-从基础到应用

JUC从实战到源码:原子类的基本操作

😄生命不息,写作不止

🔥 继续踏上学习之路,学之分享笔记

👊 总有一天我也能像各位大佬一样

🏆 博客首页 @怒放吧德德 To记录领地 @一个有梦有戏的人

🌝分享学习心得,欢迎指正,大家一起学习成长!

转发请携带作者信息 @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)

前言

上篇文章了解到了 CAS 的原理与源码,然而原子类与 CAS 是相辅相成的,接下来将会对原子类的基础到源码分析进行学习,本章先来了解一下原子类的基本操作。

首先我们可以看到 API 中,关于 juc 包下的原子类有 16 个

其中又分为基本类型、数组类型、引用类型、字段更新类。

基本类型原子类

基本类型原子类有AtomicInteger、AtomicLong、AtomicBoolean。

案例

我们通过一个案例学习基本类型原子类,通过原子类来模拟实现多个线程执行 i++ 操作,并且保证数据的正确性。

java 复制代码
public class AtomicBase {

    public static final int SIZE = 50;

    public static void main(String[] args) throws InterruptedException {
        BaseNumber baseNumber = new BaseNumber();
        CountDownLatch countDownLatch = new CountDownLatch(SIZE);
        for (int i = 1; i <= SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <= 1000; j++) {
                        baseNumber.addPlusPlus();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }, "线程" + i).start();
        }
        countDownLatch.await();
        // 等待线程计算完成
        System.out.println(Thread.currentThread().getName() + " - 计算结果 - " + baseNumber.atomicInteger.get());
    }
}

class BaseNumber {
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addPlusPlus() {
        // i++操作
        atomicInteger.getAndIncrement();
    }
}

当如果我们没有使用 CountDownLatch的时候,最后输出的结果并不是 50000,导致这样的结果并不是原子类线程不安全,而是因为,我们是在 main 线程中开了许多个子线程去执行计算,但是最后输出的语句并不是等待子线程全部执行完成蔡得到的结果。

等待线程完成有多种,也包括使用线程的 Sleep进行等待线程执行完毕,但是,实际上的业务中,不一定知道线程能多久执行完毕,所以就引出了 CountDownLatch类。

CountDownLatch

Java中的CountDownLatch是一种同步工具类,用于协调多个线程之间的执行顺序。它允许一个或多个线程等待其他线程完成操作后再继续执行。

关键的方法是 void await()阻塞当前线程,直到计数器归零,以及 void countDown()计数器减1,表示一个事件已完成。使用 await 也是支持设置超时时间。

数组类型原子类

数组原子类型有:AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray

案例

数组类型的原子类与基本原子类是一致的,也就是基本原子类的数组形式,我们通过以下例子来学习。

java 复制代码
public class AutoArrayDemo {
    public static void main(String[] args) {
        // 三种构造方法
//        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
//        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});
        for (int i = 0; i < atomicIntegerArray.length(); i++) {
            System.out.println(atomicIntegerArray.get(i));
        }
        int index = 1;
        int value = 0;
        // 获取在增加
        value = atomicIntegerArray.getAndAdd(index, 2025);
        System.out.println("下标:" + index + " - 原始值:" + value + " - 当前值:" + atomicIntegerArray.get(index));
        // 执行i++操作
        value = atomicIntegerArray.getAndIncrement(index);
        System.out.println("下标:" + index + " - 原始值:" + value + " - 当前值:" + atomicIntegerArray.get(index));
    }
}
// 输出
1
2
3
4
5
下标:1 - 原始值:2 - 当前值:2027
下标:1 - 原始值:2027 - 当前值:2028

以上代码简单实现 AtomicIntegerArray的三种初始化,以及遍历获取数据,通过下标执行增加以及 i++操作,底层也是运用到了 CAS (compareAndSwapInt),也就是 AtomicInteger的数组形式。

引用类型原子类

引用类型原子类是最重要的 API,分别有 AtomicReference<T>AtomicStampedReference<T>AtomicMarkableReference<T>。由于基本类型都是对 Integer、Long 类型来进行 CAS 操作,那么我们想要对其他类进行 CAS 操作呢,JDK 就提供了引用类型的原子类来使我们实现 CAS 操作。

典型案例-自旋锁

通过使用引用类型原子类的典型案例就是自旋锁,将线程类(Thread)带入引用原子类AtomicReference。(也就是上篇文章提到的自旋锁例子)

java 复制代码
public class SpinLockDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void lock() {
        // 获取当前线程
        Thread currentThread = Thread.currentThread();
        while (!atomicReference.compareAndSet(null, currentThread)) {
//            System.out.println(currentThread.getName() + " 自旋中...");
        }
        System.out.println(currentThread.getName() + " 获取锁");
    }

    public void unLock() {
        // 获取当前线程
        Thread currentThread = Thread.currentThread();
        atomicReference.compareAndSet(currentThread, null);
        System.out.println(currentThread.getName() + " 释放锁");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.lock();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            spinLockDemo.unLock();
        }, "T1").start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        new Thread(() -> {
            spinLockDemo.lock();
            spinLockDemo.unLock();
        }, "T2").start();
    }
}
// 执行结果
T1 获取锁
// .... 过了5秒
T1 释放锁
T2 获取锁
T2 释放锁

上篇文章介绍了AtomicReference 实现自旋锁,也提到了会遇到 ABA 问题,但是也对 ABA 的问题通过AtomicStampedReference(带戳记版本号来实现),通过每次的调用,对版本号进行+1 操作,这个能够解决操作多少次的问题。

AtomicMarkableReference

主要是用于多线程并发情况下以原子方式更新对象的引用,是带具有布尔标记位,通过标记来解决 CAS 中带来的 ABA 问题。它适用于需要同时维护对象的引用和一个简单状态标记的场景。简单来说就是解决有没有修改过。

简单看一下案例

java 复制代码
public class AtoMarkDemo {
    static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(99, false);
    public static void main(String[] args) {
        new Thread(() -> {
            // 获取当前的标记
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println("当前的标记:" + marked);
            // 睡眠是为了使t2获得初始值是false
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 调用CAS, 期望是99,修改成100,并且把标记取反
            boolean b = atomicMarkableReference.compareAndSet(99, 100, marked, !marked);
            System.out.println("t1修改是否成功:" + b + " 当前值: " + atomicMarkableReference.getReference());
        }, "t1").start();

        new Thread(() -> {
            // 获取当前的标记
            boolean marked = atomicMarkableReference.isMarked();
            System.out.println("当前的标记:" + marked);
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 调用CAS, 期望是99,修改成100,并且把标记取反
            boolean b = atomicMarkableReference.compareAndSet(99, 100, marked, !marked);
            System.out.println("t2修改是否成功:" + b + " 当前值: " + atomicMarkableReference.getReference() + " 之前是否有动过:" + atomicMarkableReference.isMarked());
        }, "t2").start();
    }
}
// 结果
当前的标记:false
当前的标记:false
t1修改是否成功:true 当前值: 100
t2修改是否成功:false 当前值: 100 之前是否有动过:true

对象的属性修改原子类

原子更新对象字段主要有三种:

  • AtomicIntegerFieldUpdater<T>更新对象中的 int 类型的字段值
  • AtomicLongFieldUpdater<T>更新对象中的 long 类型的字段值
  • AtomicReferenceFieldUpdater<T>更新对象中的引用类型字段值

我们通过 JavaAPI 文档中可以看到描述,它们的共性都是基于反射的实用程序,可以对指定类的指定的volatile 修饰的字段进行原子更新。也就是它是一种更加细粒度的更新。

使用目的:

它是以一种线程安全的方式操作非线程安全对象内的某些字段。

使用要求:

  • 更新的对象属性中必须使用 public volatile 修饰。
  • 因为对象的属性修改类型原子类都是抽象类,所以每次使用必须使用静态方法(newUpdater())创建一个更新器,并且需要设置想要更新的类和属性。

案例

定义类 Count,里面包含了一个 public volatile 修饰的变量 count,原本来说,我们在多线程的情况下需要通过使用 synchronized 修饰自增方法,来确保并发带来的问题,但是性能消耗比较大,我们可以通过使用AtomicIntegerFieldUpdater 来对类中的某个字段进行线程安全操作。

java 复制代码
public class AtoFieldDemo {

    public static void main(String[] args) throws InterruptedException {
        Count count = new Count();
        CountDownLatch countThread = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                try {
                    count.incrementCount(count);
                } finally {
                    countThread.countDown();
                }
            }, i + "").start();
        }
        countThread.await();
        System.out.println("1000个线程操作结束之后的值:" + count.count);
    }

}

class Count {
    public volatile int count = 0;

    AtomicIntegerFieldUpdater<Count> atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Count.class, "count");

    // 不用synchronized
    public void incrementCount(Count count) {
        atomicIntegerFieldUpdater.getAndIncrement(count);
    }
}

引用类型案例

接下来看一下引用类型的使用,通过引用类型的修改原子类,需要指定要进行原子操作的类,以及对应的属性类型,属性名称,通过静态方法构建更新器,这与 Integer 类型的是一致的。

java 复制代码
public class AtoRefDemo {
    public static void main(String[] args) {
        RefCount refCount = new RefCount();
        // 模拟多个线程并发去初始化
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                try {
                    refCount.init(refCount);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }, "T" + i).start();
        }
    }
}

class RefCount {
    public volatile Boolean isInit = Boolean.FALSE;
    // 构造更新器,类、类型、字段名
    AtomicReferenceFieldUpdater<RefCount, Boolean> atomicReferenceFieldUpdater =
            AtomicReferenceFieldUpdater.newUpdater(RefCount.class, Boolean.class, "isInit");

    public void init(RefCount refCount) throws InterruptedException {
        // 这里做初始化,用CAS来判断是否执行过
        System.out.println("线程:" + Thread.currentThread().getName() + " 即将初始化...");
        boolean b = atomicReferenceFieldUpdater.compareAndSet(refCount, Boolean.FALSE, Boolean.TRUE);
        if (b) {
            System.out.println("线程:" + Thread.currentThread().getName() + " 初始化中...");
            Thread.sleep(2000);
            System.out.println("线程:" + Thread.currentThread().getName() + " 初始化完成...");
        } else {
            System.out.println("线程:" + Thread.currentThread().getName() + " 初始化失败,已有其他线程占用...");
        }
    }
}

当有线程 CAS 成功之后,就能够进入初始化,其他线程要进行 CAS 的时候,此时值已经被改变了,CAS 就会失败(期望值与实际值不一致)。

总结

原子类在多线程编程中发挥关键作用,能确保数据操作的原子性和可见性。基本类型原子类如 AtomicInteger 等,通过 CAS 算法实现高效并发控制,适用于简单变量的线程安全操作。数组类型原子类如 AtomicIntegerArray 等,是基本类型的扩展,适用于数组元素的并发修改。引用类型原子类如 AtomicReference 等,用于对对象引用进行原子操作,自旋锁案例展示了其在控制线程获取和释放锁方面的应用,而 AtomicStampedReference 解决了 ABA 问题。对象属性修改原子类如 AtomicIntegerFieldUpdater 等,能以细粒度方式对对象特定字段进行原子更新,适用于对象内部字段的并发修改。


转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人

持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

谢谢支持!

相关推荐
浪遏7 分钟前
面试官😏: 讲一下事件循环 ,顺便做道题🤪
前端·面试
uhakadotcom10 分钟前
Pandas入门:数据处理和分析的强大工具
后端·面试·github
嗨起飞了11 分钟前
Maven快速入门指南
java·maven
Asthenia041212 分钟前
Json里面想传图片(验证码图)-Base64编码来助你!
后端
A boy CDEF girl27 分钟前
【JavaEE】线程池
java·java-ee
Joeysoda27 分钟前
JavaEE进阶(2) Spring Web MVC: Session 和 Cookie
java·前端·网络·spring·java-ee
服务端技术栈1 小时前
MySQL 索引:数据库查询的“加速器”
后端
Y雨何时停T1 小时前
深入理解 Java 虚拟机之垃圾收集
java·开发语言
Asthenia04121 小时前
Redis与MySQL协同:旁路缓存机制
后端
hamburgerDaddy11 小时前
golang 从零单排 (一) 安装环境
开发语言·后端·golang