【多线程:cas】原子类型
01.解释
JUC提供了一些实现了cas的工具类:三大类 原子整数 原子引用 原子数组
02.原子整数
有AtomicBoolean、AtomicInteger、AtomicLong,这里我们拿AtomicInteger举例
AtomicInteger i = new AtomicInteger(5);
System.out.println(i.incrementAndGet()); // ++i
System.out.println(i.getAndIncrement()); // i++
System.out.println(i.decrementAndGet()); // --i
System.out.println(i.getAndDecrement()); // i--
System.out.println(i.getAndAdd(5)); // 先打印再自增5
System.out.println(i.addAndGet(5)); // 先自增5再打印
i.updateAndGet(value -> value * 10);
// 内部是函数式接口 可以使用lambda表达式
// 可以自定义运算方式,更新但没有返回值
i.getAndUpdate(value -> value * 10);
// 返回更新后的返回值
上述都是AtomicInteger类常用的方法 方法对应的操作都是原子的 不会出现线程安全问题 注释中有对应的操作。
03.原子引用
补充
在学下面内容之前,我们要明确一个概念,cas操作中的比较的是地址值或者地址的内容?答案是地址值,下面的ABA问题会体现。
为什么要有原子引用类型
我们知道类型分为基本类型(int long char等)和引用类型(类对象),上述我们的原子整数对应的就是基本类型的cas实现,现在的原子引用就是引用类型的cas实现。
AtomicReference< Object>
介绍
泛型里放要使用的引用类型 保证它的原子性
例子
还是上一篇文章的取钱例子,只不过把int类型换为了BigDecimal类型,我们现在是否还会出现线程安全问题
代码
@Slf4j(topic = "c.Test35")
public class Test35 {
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
}
}
class DecimalAccountCas implements DecimalAccount {
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
// this.balance = 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 = prev.subtract(amount);
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
}
interface DecimalAccount {
// 获取余额
BigDecimal getBalance();
// 取款
void withdraw(BigDecimal amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}
结果
0
解释
可以看出没有出现线程安全问题
ABA问题
介绍
ABA问题是指 我们现在有三个线程 t1 t2 t3,t1线程想要把字符A更新为C 但是在此之前t2线程把A更新为B,t3线程把B更新为A,问题是:此时t1线程的cas操作是否可以成功 我们上述已经介绍了cas操作比较的是地址 理论上来说cas操作不会成功,但是事实上这个操作是可以成功的
@Slf4j(topic = "c.TestABA")
public class TestABA {
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
String prev = ref.get();
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C"));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
}
结果
15:25:51.824 c.TestABA [main] - main start...
15:25:51.865 c.TestABA [t1] - change A->B true
15:25:52.373 c.TestABA [t2] - change B->A true
15:25:53.381 c.TestABA [main] - change A->C true
解释
我们可以看见最后一次cas操作成功了,那有人会想应该这里比较的类型是String,我们又知道String类型的equals方法进行了重写 只要值相同算相同,但还有人认为这里比较不是用equals比较的而是用 == 比较的,比较的是地址 至于为什么相同是因为 我们的"A"是从常量池中获取的 也就是s1 == "A"与s2 == "A" 的地址值都是"A"的地址 所以比较才会相同 进而交换成功。
探究cas操作比较的是equals还是==
还是上面那个代码 只不过这次我们直接创建一个String对象 而不是采用从常量池获取的方式,看看这次的ABA是否会成功
@Slf4j(topic = "c.TestABA")
public class TestABA {
static AtomicReference<String> ref = new AtomicReference<>(new String("A"));
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
String prev = ref.get();
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, new String("C")));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.get(), new String("B")));
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.get(), new String("A")));
}, "t2").start();
}
}
结果
15:36:40.437 c.TestABA [main] - main start...
15:36:40.473 c.TestABA [t1] - change A->B true
15:36:40.981 c.TestABA [t2] - change B->A true
15:36:41.985 c.TestABA [main] - change A->C false
解释
我们发现这次cas操作竟然失败了,说明了什么?说明cas操作比较的是 == 而不是equals,因为如果是equals 那么s1 == new String("A")与s2 == new String("A")的比较结果应该相同 cas操作应该成功。综上我们得出了cas操作比较的是 ==
如何解决ABA问题:AtomicStampedReference
介绍
AtomicStampedReference是AtomicReference的升级,它加入了计时器(版本号) 每进行一次操作计时器就会改变,最后进行cas对比时 比较的不仅仅是地址还有计时器是否相同
对上述代码优化
@Slf4j(topic = "c.Test36")
public class Test36 {
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();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1));
log.debug("更新版本为 {}", ref.getStamp());
}, "t2").start();
}
}
结果
15:42:58.652 c.Test36 [main] - main start...
15:42:58.655 c.Test36 [main] - 版本 0
15:42:58.709 c.Test36 [t1] - change A->B true
15:42:58.709 c.Test36 [t1] - 更新版本为 1
15:42:59.213 c.Test36 [t2] - change B->A true
15:42:59.214 c.Test36 [t2] - 更新版本为 2
15:43:00.227 c.Test36 [main] - change A->C false
解释
我们发现这次交换失败,原因就是上面所讲增加了计时器,之后的每一次cas操作不仅需要传 最新值和要更新的值 还需要传 版本号和要更新的版本号,最终解决了ABA问题。
注意
因为基本类型的地址值就是它们的值本身 所以原子整数这一类 也会发生ABA问题
AtomicMarkableReference
AtomicMarkableReference方法和AtomicStampedReference基本一致,不同的是AtomicMarkableReference方法把计时器改为了boolean类型 也就是标记,如果改过就改变标记 最后比较时 比较一下标记是否一样,不过这里有一个问题 就是 因为boolean只有两种状态true与false 也就是它只能保证解决AB问题或者降低ABA问题出现的几率,但是不能解决ABA问题。
AtomicMarkableReference方法因为和AtomicStampedReference方法用法基本一致这里不再举例。
04.原子数组
AtomicIntegerArray:原子整型数组
AtomicLongArray:原子长整型数组
AtomicReferenceArray:原子引用数组
这里我们拿AtomicIntegerArray举例
介绍
为什么需要原子数组,因为我们之前的数组在新增元素的时候是线程不安全的。
原子数组使用
例子介绍
创建十个线程 与一个 公共的int数组长度为10,每个线程分别往对应的数组下标元素里面加10000次1,例如线程0就向a[0]加1,最后打印数组,理论上的结果应该是数组的十个元素都是10000。
代码
下面代码用到了lambda函数式接口的知识,如果不会的同学可以看我之前的lambda表达式的文章
https://blog.csdn.net/m0_71229547/article/details/125046910?spm=1001.2014.3001.5501
https://blog.csdn.net/m0_71229547/article/details/125093487?spm=1001.2014.3001.5501
public class Test39 {
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)
);
}
/**
参数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);
}
}
结果
[9335, 9298, 9263, 9254, 9318, 9260, 9248, 9229, 9230, 9242]
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
解释
可以看出普通数组存在并发安全问题,但是原子数组并没有这样的问题。
原子数组的ABA问题
ABA出现的原因
我们先来分析为什么会有ABA问题,究其根本就是cas比较时地址没有改变 但其实以及改变过了。
原子数组的cas实现
我们注意到compareAndSet的3个参数,i代表的是数组偏移值 expect代码的是数组偏移值对应元素的旧值 update代表的是偏移值对应元素的要更新的值。
我们注意几个点
1.原子数组cas比较的是数组里面偏移值对应的元素,而不是数组与数组之间的比较
2.既然是数组元素的比较,所以因为数组类型的不同就有可能出现ABA问题
例子
@Slf4j(topic = "c.TestABA")
public class TestABA {
static AtomicIntegerArray ref = new AtomicIntegerArray(10);
public static void main(String[] args) throws InterruptedException {
log.debug("main start...");
// 获取值 A
int prev = ref.get(1);
other();
sleep(1);
// 尝试改为 C
log.debug("change A->C {}", ref.compareAndSet(1,prev,2));
}
private static void other() {
new Thread(() -> {
log.debug("change A->B {}", ref.compareAndSet(1,ref.get(1),1));
}, "t1").start();
sleep(0.5);
new Thread(() -> {
log.debug("change B->A {}", ref.compareAndSet(1,ref.get(1), 0));
}, "t2").start();
}
}
结果
16:41:43.219 c.TestABA [main] - main start...
16:41:43.274 c.TestABA [t1] - change 0->1 true
16:41:43.790 c.TestABA [t2] - change 1->0 true
16:41:44.790 c.TestABA [main] - change 0->2 true
解释
可以看出用原子整数数组类型 发生了ABA问题,究其根本就是因为AtomicIntegerArray内部实现是int为基本类型所以会出现cas比较成功的问题,那我们用AtomicReferenceArray 原子引用数组 会有这个问题吗?一样会有
如何解决原子数组的ABA问题
AtomicReferenceArray<>的泛型中填AtomicStampedReference类型,这样保证在数组元素比较时 有版本号保证 不会出现ABA问题。