CAS Krains 2020-08-25
无锁实现多线程并发安全问题
class AccountSafe implements Account {
private AtomicInteger balance;
public AccountSafe(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
// 对比和交换,如果prev的值与主存中的值一样,则修改,返回true
// 否者则放弃修改同时返回false
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
}
其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),该方法是原子的。
工作方式/实现原理
CAS的底层是lock cmpxchg
指令,在单核和多核CPU下都能够保证比较与交换的原子性。在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性。
CAS操作依赖于volatile
原子类中用来存值的变量前加了volatile
关键字
private volatile int value;
1
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。
它可以用来修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存,即一个线程对volatile变量的修改,对另一个线程可见。
CAS与synchronized比较
- CAS是基于乐观锁的思想,最乐观的估计,不怕别的线程来改。synchronized是基于悲观锁的思想,防着别的线程来改。
- CAS与volatile实现的是无锁并发、无阻塞并发,synchronized是有锁并发、阻塞并发
- CAS较synchronized来说线程上下文切换没那么频繁,synchronized中一个线程没有获得到锁就会进入阻塞状态,会涉及到上下文的切换,CAS就不会进入阻塞状态,但如果线程数多了CAS也会有上下文切换频繁的问题,因为每个CPU一次只能执行一个线程,线程多了就会进入就绪态。
JUC提供的原子类
原子整数类
- AtomicBoolean
- AtomicInteger
- AtomicLong
以AtomicInteger为例使用,以下是源码的重要方法:
// volatile保证变量的可见性,每次从主存中读value,写到主存
private volatile int value;
// cas操作,如果主存中的值和expect不一致,则设置失败,返回false
// 如果一致,则用update替换主存中的expect值,返回true
// 该操作是原子的,在多线程环境下不会发送线程安全问题
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// IntUnaryOperator是一个接口,我们可以实现该接口,自定义对value加减乘除操作
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
使用例子
AtomicInteger i = new AtomicInteger(0);
// 相当于++i操作,先对i加1,在获取i的值
System.out.println(i.incrementAndGet());
// 相当于i++操作,先获取i的值,在对i加1
System.out.println(i.getAndIncrement());
// updateAndGet接受一个函数(接口实现类),这个函数定义了对value的操作,
// 能够保证函数的操作的原子性,并且要求这个函数是无副作用的,因为它会多次执行该函数
System.out.println(i.updateAndGet(p->2*p));
无副作用是函数式编程中的一个概念,无副作用的意思就是: 一个函数(java里是方法)的多次调用中,只要输入参数的值相同,输出结果的值也必然相同,并且在这个函数执行过程中不会改变程序的任何外部状态(比如全局变量,对象中的属性,都属于外部状态),也不依赖于程序的任何外部状态。
原子引用
为什么需要原子引用?
保护其他引用类型变量的原子性
- AtomicReference
- AtomicMarkableReference
- AtomicStampedReference
以AtomicReference为例
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());
}
}
使用CAS实现线程安全
class DecimalAccountCAS implements DecimalAccount{
private AtomicReference<BigDecimal> balance;
public DecimalAccountCAS(BigDecimal balance){
this.balance = new AtomicReference<>(balance);
}
@Override
public BigDecimal getBalance() {
return balance.get();
}
@Override
public void withdraw(BigDecimal amount) {
// CAS保证操作的原子性
while(true){
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(amount);
if(balance.compareAndSet(prev, next)){
return ;
}
}
}
}
使用synchronized
,相比来说synchronized
重量级锁开销大
class DecimalAccountLock implements DecimalAccount{
private BigDecimal balance;
public DecimalAccountLock(BigDecimal balance){
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
// 注意不能锁balance,balance是会变的
synchronized (this){
balance = balance.subtract(amount);
}
}
}
ABA问题
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
// 获取值 A
// 这个共享变量被它线程修改过?
String prev = ref.get();
other();
Thread.sleep(1000);
// 尝试改为 C
System.out.println("change A->C "+ref.compareAndSet(prev, "C"));
}
private static void other() throws InterruptedException {
new Thread(() -> {
System.out.println("change A->B " + ref.compareAndSet(ref.get(), "B"));
}, "t1").start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("change B->A " + ref.compareAndSet(ref.get(), "A"));
}, "t2").start();
}
输出如下
change A->B true
change B->A true
change A->C true
主线程首先获得共享变量的值,如果此时其他线程将共享变量由A该为B,再由B该成A,此时主线程再对共享变量进行cas操作也是可以成功的,就是说AtomicReference无法感知共享变量是否被修改过,这存在一个安全隐患问题。
解决方法
使用AtomicStampedReference类,通过增加一个版本号来判断共享变量是否被修改过
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
// 如果中间有其它线程干扰,发生了 ABA 现象
other();
Thread.sleep(1000);
// 尝试改为 C
System.out.println("版本 "+stamp);
System.out.println("change A->C "+ref.compareAndSet(prev, "C", stamp, stamp + 1));
}
private static void other() throws InterruptedException {
new Thread(() -> {
System.out.println("change A->B "+ref.compareAndSet(ref.getReference(), "B",
ref.getStamp(), ref.getStamp() + 1));
System.out.println("更新版本为 "+ref.getStamp());
}, "t1").start();
Thread.sleep(500);
new Thread(() -> {
System.out.println("change B->A "+ref.compareAndSet(ref.getReference(), "A",
ref.getStamp(), ref.getStamp() + 1));
System.out.println("更新版本为 "+ref.getStamp());
}, "t2").start();
}
此时版本号不一致导致更新失败
change A->B true
更新版本为 1
change B->A true
更新版本为 2
版本 0
change A->C false
- ASP.NET MVC Controller激活系统详解:IoC的应用[上篇]
- ASP.NET Core的配置(1):读取配置信息
- 权限管理和备份实例
- “协变”、“逆变”与Delegate类型转换
- 如今的人工智能是不是真的已经很聪明了?
- 【Scikit-Learn 中文文档】聚类 - 无监督学习 - 用户指南 | ApacheCN
- Delegate如何进行类型转换?
- 个性化推荐系统(一)---今日头条等的内容划分、分类
- ASP.NET Core的配置(2):配置模型详解
- 如何解决jQuery Validation针对动态添加的表单无法工作的问题?
- 数据结构 链表改进
- 数据结构 栈&队列
- 终端品牌域名过期被拍卖 价值六位数
- TensorFlow 深度学习笔记 TensorFlow实现与优化深度神经网络
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 微信头像加国旗
- FlutterDojo设计之道—状态管理之路(四)
- FlutterDojo设计之道—状态管理之路(五)
- Python高级进阶(一)Python框架之Django入门
- python 自动化测试(2):针对密码输入框为密码控件时的解决方案
- 微信小程序开发之视图容器swiper
- WINDOWS服务器同时安装多个版本的MYSQL的方法,MQYSQL5和MQYSQL8的共存。
- 使用PHP对接国外的PAYPAL支付最新REST API接口。
- golang 学习(10): 使用go语言调用c语言的so动态库
- Django的form,model自定制
- 排序算法与查找算法
- nginx学习(1): openresty安装
- 2020年8月26日更新WINDOWS服务器同时安装多个版本的MYSQL的方法,MQYSQL5和MQYSQL8的共存。
- python 自动化测试(3):数据库连接与使用
- 微信小程序实战开发二:微信小程序 WEUL组件之 画廊组件