Java多线程共享模型之乐观锁(CAS与Atomic原子类)
文章目录
乐观锁的概念是相对于轻量级锁、偏向锁、重量级锁而言的
乐观锁本身是一种有锁似无锁的状态
CAS需要配合valiate使用,即保证共享变量的可见性
背景
有个账户,有两个功能,取款和查询余额,如何保证多线程同时取款不会出现并发问题?
账户接口:
interface DecimalAccount{
//获取余额
BigDecimal getBalance() ;
//取款
void withdraw(BigDecimal account) ;
//模拟多线程取款
static void demo(DecimalAccount account){
List<Thread> ts = new ArrayList<>() ;
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(()->{
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
ts.forEach(t->{
t.join();
System.out.println(account.getBalance());
}
}
线程不安全的实现:
public void withdraw(BigDecimal amount) {
BigDecimal balance = this.getBalance() ;
this.balance = balance.subtract(amount) ;
}
}
解决方案一:重量锁
synchronized (lock){
BigDecimal balance = this.getBalance() ;
this.balance = balance.subtract(amount) ;
}
解决方案二:采用CAS操作(不加锁/乐观锁)
//CAS
AtomicReference<BigDecimal> ref ;
public BigDecimal getBalance() {
return ref.get() ;
}
@Override
public void withdraw(BigDecimal amount) {
while (true){
BigDecimal prev = ref.get() ;
BigDecimal next = prev.subtract(amount) ;
if (ref.compareAndSet(prev,next)) break;
}
CAS分析
前面看到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
主要是这一句
if (ref.compareAndSet(prev,next)) break;
- compareAndSet 正是做这个检查,在 set 前,先比较 prev 与ref在内存中的最新值
- 若不一致,next 作废,返回 false 表示失败。比如,别的线程已经做了减法,当前值已经被减成了 990,那么本线程的这次 990 (next)就作废了,进入 while 下次循环重试
- 若一致,以 next 设置为新值,返回 true 表示成功
compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作
注意:
CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
在多核状态下,某个核执行到带
lock
的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。《计组-总线嗅探机制》
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
CAS在竟态条件不严重时效率高
synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞
-
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
-
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
_Atomic原子类
ABA问题
所谓ABA,就是线程1要将"A"改为"C",假定线程2在线程1执行完毕之前把“A”改为“B”又改为“A”。 此时线程1无法得知共享变量”A“被修改过,CAS执行仍会成功
public class TestABA {
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始");
String prev = ref.get() ;
//A->B->A
AtoBtoA();
Thread.sleep(1000);
//change A -> C
//prev = A1 实际上内存中是A2 CAS以为是prev是最新值
boolean c = ref.compareAndSet(prev, "C");
System.out.println("change A->C"+c);
}
private static void AtoBtoA() throws InterruptedException {
//A->B
new Thread(()->{
String prev = ref.get();
boolean b = ref.compareAndSet(prev, "B");
System.out.println("change A->B"+b);
}).start();
Thread.sleep(500);
//B->A
new Thread(()->{
String prev = ref.get();
boolean a = ref.compareAndSet(prev, "A");
System.out.println("change B->A"+a);
}).start();
}
}
//
主线程开始
change A->Btrue
change B->Atrue
change A->Ctrue
AtomicStampedReference版本号
希望:
只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号
AtomicStampedReference
public class SolveABA {
//arg2(0) = 版本号
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始");
String prev = ref.getReference() ;
//获取版本号
int stamp = ref.getStamp();
System.out.println("主版本号为"+stamp);
AtoBtoA();
Thread.sleep(1000);
boolean c = ref.compareAndSet(prev, "C", stamp, stamp + 1);
//change ->C
System.out.println("change A->C"+c);
}
private static void AtoBtoA() throws InterruptedException {
new Thread(()->{
String prev = ref.getReference();
boolean b = ref.compareAndSet(prev, "B", ref.getStamp(), ref.getStamp() + 1);
System.out.println("change A->B"+b);
}).start();
Thread.sleep(500);
new Thread(()->{
String prev = ref.getReference();
boolean a = ref.compareAndSet(prev, "A", ref.getStamp(), ref.getStamp() + 1);
System.out.println("change B->A"+a);
}).start();
}
}
结果:
主版本号为0
change A->Btrue
change B->Atrue
change A->Cfalse
通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次
原子数组AtomicIntegerArray
原子整数只对单个值的共享变量有用,但并不保证集合、数组内元素的线程安全,可以使用AtomicIntegerArray保证集合或数组内各元素线程安全
以下代码可以测试数组是否线程安全,该方法将启动多个线程对数组内各个元素进行自增操作
如数组内十个元素初始值为0,十个线程将数组内十个元素自增10000次,线程安全的结果应该为
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
public class TestAtomicArray {
/*测试数组安全性的通用方法,采用函数式编程提供参数即可*/
/**
参数1,提供数组、可以是线程不安全数组或线程安全数组
参数2,获取数组长度的方法
参数3,自增方法,回传 array, index
参数4,打印数组的方法
*/
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() ;
Integer 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 -> {
t.join();
});
printConsumer.accept(array);
}
不安全验证:
public static void main(String[] args) {
demo(
()->new int[10],
(array)->array.length,
(array,index) -> array[index]++
array-> System.out.println(Arrays.toString(array))
);
}
}
结果:
[9224, 9254, 9278, 9262, 9248, 9252, 9278, 9280, 9233, 9293]
安全验证:
//安全的
demo(
()->new AtomicIntegerArray(10),
(array)->array.length(),
(array,index)->array.getAndIncrement(index),
array-> System.out.println(array)
);
结果:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
原子类常见操作
package com.Thread;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class TestAutomic {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0) ;
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println(i.getAndIncrement());
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
System.out.println(i.incrementAndGet());
/* 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
其中函数中的操作能保证原子,但函数需要无副作用*/
System.out.println(i.getAndUpdate(p->p+2));
/* 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
其中函数中的操作能保证原子,但函数需要无副作用*/
System.out.println(i.updateAndGet(p -> p + 2));
/*获取并计算/计算并获取*/
//p = i ; x = 10 ;
System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
//p = i ; x = 10 ;
System.out.println(i.accumulateAndGet(10, (p, x) -> p + x));
}
}