Tim

一枚野生程序员~

  • 主页
  • 分类
  • 标签
  • 归档
  • 关于
所有文章 工具

Tim

一枚野生程序员~

  • 主页
  • 分类
  • 标签
  • 归档
  • 关于

CAS操作与ABA问题

阅读数:次 2020-04-02
字数统计: 1.6k字   |   阅读时长≈ 6分

我们在使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突, 所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS的操作过程

CAS即Compare and Swap,CAS比较交换的过程可以通俗的理解为CAS(V、A、B),包含三个值分别为: V内存中实际值、 A预期原值(旧值) 、B更新的新值。当V和A相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值A就是目前来说最新的值了,自然而然可以将新值B赋值给V。反之,V和A不相同,表明该值已经被其他线程改过了则该旧值A不是最新版本的值了,所以不能将新值B赋给V,返回V即可。当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。

CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。CAS支持原子更新操作,适用于计数器,序列发生器等场景(序列发生器就是用来给变量自增的工具),CAS属于乐观锁机制,号称lock-free(其实只是上层感知无锁,底层还是有加锁操作的)。CAS操作失败时由开发者决定是继续尝试还是执行别的操作。

基于CAS实现的工具

java.util.concurrent包都中的实现类都是基于volatile和CAS来实现的。尤其java.util.concurrent.atomic包下的原子类。 就拿AtomicInteger来说:

mark

可以看到 AtomicInteger 底层用的是volatile的变量和CAS来进行更改数据的。volatile保证可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值,CAS则保证数据更新的原子性。

CAS多数情况下对开发者来说是透明的。J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选。Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有隐患。JDK9以后,可以使用Variable Handle API来替代Unsafe

模拟实现CAS

CAS需要硬件层面的支持,所以模拟还是用synchronized来实现一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class TestImplementCAS {
public static void main(String[] args) {
final CompareAndSwap cas = new CompareAndSwap();

for (int i = 0; i < 10; i++) {
new Thread(()->{
int expectedValue = cas.get();
boolean b = cas.compareAndSet(expectedValue, (int)(Math.random() * 101));
System.out.println(b);
}).start();
}
}
}

class CompareAndSwap{
private int value;

//获取内存值
public synchronized int get(){
return value;
}

//比较
public synchronized int compareAndSwap(int expectedValue, int newValue){
int oldValue = value;

if(oldValue == expectedValue){
this.value = newValue;
}

return oldValue;
}

//设置
public synchronized boolean compareAndSet(int expectedValue, int newValue){
return expectedValue == compareAndSwap(expectedValue, newValue);
}
}

mark

CAS的缺点

1、ABA问题

因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。在JDK1.5后的atomic包中提供 了AtomicStampedReference来解决ABA问题,解决思路就是这样的。如果需要解决ABA问题,互斥与同步可能比CAS更高效。

2、自旋会浪费大量的处理器资源
与线程阻塞相比,自旋会浪费大量的处理器资源。这是因为当前线程仍处于运行状况,只不过跑的是无用指令。它
期望在运行无用指令的过程中,锁能够被释放出来。JVM给出的方案是自适应自旋,根据以往自旋等待时能否获取锁,来动态调整自旋的时间。

3、公平性问题
自旋状态还带来另外一个副作用,不公平的锁机制。处于阻塞状态的线程,无法立刻竞争被释放的锁。然而,处于
自旋状态的线程,则很有可能优先获得这把锁。内建锁无法实现公平机制,而lock体系可以实现公平锁。

解决ABA问题

在JDK1.5后的atomic包中提供 了AtomicStampedReference来解决ABA问题, 它通过包装[E,Integer]的元组来对对象标记版本stamp,从而避免ABA问题。在了解AtomicStampedReference之前我们可以先分析一下AtomicReference。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {
public static void main(String[] args) {
AtomicReference<String> atomicReference = new AtomicReference<>();
atomicReference.set("AAA");

//CAS操作更新
boolean result = atomicReference.compareAndSet("AAA", "BBB");
System.out.println(result + " " + atomicReference.get());

//CAS操作更新
result = atomicReference.compareAndSet("AAA", "CCC");
System.out.println(result + " " + atomicReference.get());
}
}

AtomicReference的成员变量

1
2
3
4
5
6
7
8
//Unsafe类提供CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();

//value变量的偏移地址,这个偏移地址在static块里初始化
private static final long valueOffset;

//实际传入需要原子操作的那个类实例
private volatile V value;

compareAndSet方法是基于Unsafe提供的compareAndSwapObject方法, 这里的compareAndSet方法即CAS操作本身是原子的,但是在某些场景下会出现异常场景,也就是ABA问题。我们使用AtomicStampedReference来解决这个问题,下面是AtomicStampedReference的关键结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference; //维护对象引用
final int stamp; //用于标志版本

private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}

static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}

private volatile Pair<V> pair;
...
}
赏

谢谢你请我喝咖啡

支付宝
微信
  • 本文作者: Tim
  • 本文链接: https://zouchanglin.cn/892077598.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!
  • JavaSE
  • JUC
  • 并发编程

扫一扫,分享到微信

谈谈Java的线程池
ReentrantLock与AQS
  1. 1. CAS的操作过程
  2. 2. 基于CAS实现的工具
  3. 3. 模拟实现CAS
  4. 4. CAS的缺点
  5. 5. 解决ABA问题
© 2017-2021 Tim
本站总访问量次 | 本站访客数人
  • 所有文章
  • 工具

tag:

  • 生活
  • Android
  • 索引
  • MySQL
  • 组件通信
  • Nginx
  • JavaSE
  • JUC
  • JavaWeb
  • 模板引擎
  • 前端
  • Linux
  • 计算机网络
  • Docker
  • C/C++
  • JVM
  • 上传下载
  • JavaEE
  • SpringCloud
  • Golang
  • Gradle
  • 网络安全
  • 非对称加密
  • IDEA
  • SpringBoot
  • Jenkins
  • 字符串
  • vim
  • 存储
  • 文件下载
  • Mac
  • Windows
  • NIO
  • RPC
  • 集群
  • 微服务
  • SSH
  • 配置中心
  • XML
  • Chrome
  • 压力测试
  • Git
  • 博客
  • 概率论
  • 排序算法
  • 分布式
  • 异常处理
  • 文件系统
  • 哈希
  • openCV
  • 栈
  • 回溯
  • SpringCore
  • 流媒体
  • rtmp
  • 面向对象
  • Vue
  • ElementUI
  • 软件工程
  • 异步
  • 自定义UI
  • ORM框架
  • 模块化
  • 交互式
  • Jsoup
  • Http Client
  • LRUCache
  • RabbitMQ
  • 消息通信
  • 服务解耦
  • 负载均衡
  • 权限
  • 多线程
  • 单例模式
  • Protobuf
  • 序列化
  • Python
  • m3u8
  • 堆
  • 二叉树
  • 自定义View
  • 观察者模式
  • 设计模式
  • 线程池
  • 动态扩容
  • 高可用
  • GC
  • ffmpeg
  • SpringMVC
  • REST
  • Redis
  • 缓存中间件
  • UML
  • Maven
  • Netty
  • 高性能网络
  • IPC通信
  • IO
  • Stream
  • 发布订阅
  • SQLite
  • Hash
  • 集合框架
  • 链表
  • Lambda
  • 汇编语言
  • 组件化
  • Router
  • 开发工具

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia-plus根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • 思维导图
  • PDF工具
  • 无损放大
  • 代码转图
  • HTTPS证书