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 也是支持设置超时时间。
数组类型原子类
数组原子类型有:AtomicIntegerArray
、AtomicLongArray
、AtomicReferenceArray
案例
数组类型的原子类与基本原子类是一致的,也就是基本原子类的数组形式,我们通过以下例子来学习。
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 等,能以细粒度方式对对象特定字段进行原子更新,适用于对象内部字段的并发修改。
转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人
持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。转载请携带链接,转载到微信公众号请勿选择原创,谢谢!
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍
谢谢支持!