注:【问题提出】的乐观锁方案看不懂没关系,这正是本文要讨论的内容
文章目录
- Java多线程共享模型之乐观锁(CAS与Atomic原子类)
- 问题提出
- CAS分析
- 为什么无锁(CAS)效率高
- CAS特点
- JUC_Atomic原子类
- ABA问题
- ABA解决方案-AtomicStampedReference
- 原子数组AtomicIntegerArray
- 字段更新器AtomicIntegerFieldUpdater(了解)
- 原子类常见操作(了解)
有个账户,有两个功能,取款和查询余额,如何保证多线程同时取款不会出现并发问题?
账户接口:
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->{
try {
t.join(); //同步下面的打印语句
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}
线程不安全的实现:
public class TestAtomicRef implements DecimalAccount {
BigDecimal balance ;
public TestAtomicRef(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance ;
}
/*不安全实现*/
@Override
public void withdraw(BigDecimal amount) {
BigDecimal balance = this.getBalance() ;
this.balance = balance.subtract(amount) ;
}
}
//调用函数
class Test{
public static void main(String[] args) {
TestAtomicRef ref = new TestAtomicRef(new BigDecimal(10000)) ;
DecimalAccount.demo(ref);
}
}
预期结果为0,产生结果大概率不为0,故线程不安全
解决方案一:加锁
//重量锁
private final Object lock = new Object();
/*安全实现 - 重量级锁*/
@Override
public void withdraw(BigDecimal amount) {
synchronized (lock){
BigDecimal balance = this.getBalance() ;
this.balance = balance.subtract(amount) ;
}
保证取款操作完全同步
解决方案二:采用CAS操作(不加锁/乐观锁)
//CAS
AtomicReference<BigDecimal> ref ;
public TestAtomicRef(BigDecimal balance) {
ref = new AtomicReference<>(balance) ;
}
@Override
public BigDecimal getBalance() {
return ref.get() ;
}
/*安全实现 - CAS*/
@Override
public void withdraw(BigDecimal amount) {
while (true){
BigDecimal prev = ref.get() ;
BigDecimal next = prev.subtract(amount) ;
if (ref.compareAndSet(prev,next)) break;
}
}
前面看到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
主要是这一句
if (ref.compareAndSet(prev,next)) break;
compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作
注意:
CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态
在多核状态下,某个核执行到带
lock
的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。《计组-总线嗅探机制》
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
–valatile相关内容参见:
[Java线程]volatile关键字小结
Valatile原理-内存屏障
打个比喻
线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
所谓ABA,就是线程1要将"A"改为"C",假定线程2在线程1执行完毕之前把“A”改为“B”又改为“A”。 此时线程1无法得知共享变量”A“被修改过,CAS执行仍会成功,请看下面模拟代码
package com.Thread;
import java.util.concurrent.atomic.AtomicReference;
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() ;
AtoBtoA();
Thread.sleep(1000);
//change ->C
boolean c = ref.compareAndSet(prev, "C");
System.out.println("change A->C"+c);
}
private static void AtoBtoA() throws InterruptedException {
new Thread(()->{
String prev = ref.get();
boolean b = ref.compareAndSet(prev, "B");
System.out.println("change A->B"+b);
}).start();
Thread.sleep(500);
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
主线程仅能判断出共享变量的值与最初值 A 是否相同,不能感知到这种从 A 改为 B 又 改回 A 的情况,如果主线程
希望:
只要有其它线程【动过了】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号
AtomicStampedReference
package com.Thread;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class SolveABA {
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 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A -> B -> A -> C ,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次
原子整数只对单个值的共享变量有用,但并不保证集合、数组内元素的线程安全,可以使用AtomicIntegerArray保证集合或数组内各元素线程安全
以下代码可以测试数组是否线程安全,该方法将启动多个线程对数组内各个元素进行自增操作
如数组内十个元素初始值为0,十个线程将数组内十个元素自增10000次,线程安全的结果应该为
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
package com.Thread;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
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++) {
//每个线程对数组作1000次操作
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);
}
不安全验证:
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.AtomicIntegerFieldUpdater;
public class TestFieldUpdater {
private volatile int field ;
public static void main(String[] args) {
AtomicIntegerFieldUpdater fieldUpdater
= AtomicIntegerFieldUpdater.newUpdater(TestFieldUpdater.class,"field") ;
TestFieldUpdater updater = new TestFieldUpdater();
fieldUpdater.compareAndSet(updater,0,10);
// 修改成功 field = 10
System.out.println(updater.field);
// 修改成功 field = 20
fieldUpdater.compareAndSet(updater,10,20);
System.out.println(updater.field);
// 修改成功 field = 20
fieldUpdater.compareAndSet(updater,10,30);
System.out.println(updater.field);
}
}
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));
}
}