乐观锁

一般而言,在并发情况下我们必须通过一定的手段来保证数据的准确性,如果没有做好并发控制,就可能导致脏读、幻读和不可重复度等一系列问题。乐观锁是人们为了应付并发问题而提出的一种思想,具体的实现则有多种方式。

乐观锁假设数据一般情况下不会造成冲突,只在数据进行提交更新时,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,可以提高程序的吞吐量。

CAS

CAS(Compare And Swap)比较并交换,是一种实现了乐观锁思想的并发控制技术。CAS 算法的过程是:它包含 3 个参数 CAS(V,E,N),V 表示要更新的变量(内存值),E 表示旧的预期值,N 表示即将更新的预期值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,说明已经有其他线程做了更新,则当前线程什么也不做,并返回当前 V 的真实值。整个操作是原子性的。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并允许再次尝试,当然也可以放弃本次操作,所以 CAS 算法是非阻塞的。基于上述原理,CAS 操作可以在不借助锁的情况下实现合适的并发处理。

ABA 问题

ABA 问题是 CAS 算法的一个漏洞。CAS 算法实现的一个重要前提是:取出内存中某时刻的数据,并在下一时刻比较并替换,在这个时间差内可能会导致数据的变化。

假设有两个线程,分别要对内存中某一变量做 CAS 操作,线程一先从内存中取出值 A,线程二也从内存中取出值 A,并把值从 A 变为 B 写回,然后又把值从 B 变为 A 写回,这时候线程一进行 CAS 操作,发现内存中的值还是 A,于是认为和预期值一致,操作成功。尽管线程一的 CAS 操作成功,但并不代表这个过程就没有问题。

ABA 问题会带来什么隐患呢?维基百科给出了详细的示例:假设现有一个用单链表实现的堆栈,栈顶为 A,A.next = B,现有线程一希望用 CAS 把栈顶替换为 B,但在此之前,线程二介入,将 A、B 出栈,再压入 D、C、A,整个过程如下

此时 B 处于游离转态,轮到线程一执行 CAS 操作,发现栈顶仍为 A,CAS 成功,栈顶变为 B,但实际上 B.next = null,即堆栈中只有 B 一个元素,C 和 D 并不在堆栈中,平白无故就丢了。简单来说,ABA 问题使我们漏掉某一段时间的数据监控,谁知道在这段时间内会发生什么有趣(可怕)的事呢?

可以通过版本号的方式来解决 ABA 问题,每次执行数据修改操作时,都会带上一个版本号,如果版本号和数据的版本一致,对数据进行修改操作并对版本号 +1,否则执行失败。因为每次操作的版本号都会随之增加,所以不用担心出现 ABA 问题。

使用 Java 模拟 CAS 算法

这仅仅是基于 Java 层面上的模拟,真正的实现要涉及到底层(我学不会)

public class TestCompareAndSwap {

    private static CompareAndSwap cas = new CompareAndSwap();

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
// 获取预估值
int expectedValue = cas.get();
boolean b = cas.compareAndSet(expectedValue, (int) (Math.random() * 101));
System.out.println(b);
}
});
}
}
} 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);
}
}

原子类

原子包 java.util.concurrent.atomic 提供了一组原子类,原子类的操作具有原子性,一旦开始,就一直运行直到结束,中间不会有任何线程上下文切换。原子类的底层正是基于 CAS 算法实现线程安全。

Java 为我们提供了十六个原子类,可以大致分为以下四种:

1. 基本类型

  • AtomicBoolean

    原子更新布尔类型,内部使用 int 类型的 value 存储 1 和 0 表示 true 和 false,底层也是对 int 类型的原子操作

  • AtomicInteger

    原子更新 int 类型

  • AtomicLong

    原子更新 long 类型

2. 引用类型

  • AtomicReference

    原子更新引用类型,通过泛型指定要操作的类

  • AtomicMarkableReference

    原子更新引用类型,内部维护一个 Pair 类型(静态内部类)的成员属性,其中有一个 boolean 类型的标志位,避免 ABA 问题

    private static class Pair<T> {
    final T reference;
    final boolean mark;
    private Pair(T reference, boolean mark) {
    this.reference = reference;
    this.mark = mark;
    }
    static <T> Pair<T> of(T reference, boolean mark) {
    return new Pair<T>(reference, mark);
    }
    } private volatile Pair<V> pair;
  • AtomicStampedReference

    原子更新引用类型,内部维护一个 Pair 类型(静态内部类)的成员属性,其中有一个 int 类型的邮戳(版本号),避免 ABA 问题

    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;

3. 数组类型

  • AtomicIntegerArray

    原子更新 int 数组中的元素

  • AtomicLongArray

    原子更新 long 数组中的元素

  • AtomicReferenceArray

    原子更新 Object 数组中的元素

4. 对象属性类型

用于解决对象的属性的原子操作

  • AtomicIntegerFieldUpdater

    原子更新对象中的 int 类型字段

  • AtomicLongFieldUpdater

    原子更新对象中的 long 类型字段

  • AtomicReferenceFieldUpdater

    原子更新对象中的引用类型字段

之前提到的三种类型的使用都比较简单,查阅对应 API 即可,而对象属性类型则有一些限制:

  • 字段必须是 volatile 类型的,在线程之间共享变量时保证立即可见
  • 只能是实例变量,不能是类变量,也就是说不能加 static 关键字
  • 只能是可修改变量,不能使用 final 变量
  • 该对象字段能够被直接操作,因为它是基于反射实现的

5. 高性能原子类

Java8 新增的原子类,使用分段的思想,把不同的线程 hash 到不同的段上去更新,最后再把这些段的值相加得到最终的值。以下四个类都继承自 Striped64,对并发的优化在 Striped64 中实现

  • LongAccumulator

    long 类型的聚合器,需要传入一个 long 类型的二元操作,可以用来计算各种聚合操作,包括加乘等

  • LongAdder

    long 类型的累加器,LongAccumulator 的特例,只能用来计算加法,且从 0 开始计算

  • DoubleAccumulator

    double 类型的聚合器,需要传入一个 double 类型的二元操作,可以用来计算各种聚合操作,包括加乘等

  • DoubleAdder

    double 类型的累加器,DoubleAccumulator 的特例,只能用来计算加法,且从 0 开始计算

CAS 算法与 Java 原子类的更多相关文章

  1. Java原子类中CAS的底层实现

    Java原子类中CAS的底层实现 从Java到c++到汇编, 深入讲解cas的底层原理. 介绍原理前, 先来一个Demo 以AtomicBoolean类为例.先来一个调用cas的demo. 主线程在f ...

  2. 对Java原子类AtomicInteger实现原理的一点总结

    java原子类不多,包路径位于:java.util.concurrent.atomic,大致有如下的类: java.util.concurrent.atomic.AtomicBoolean java. ...

  3. Java原子类AtomicInteger实现原理的一点总结

    java原子类不多,包路径位于:java.util.concurrent.atomic,大致有如下的类: java.util.concurrent.atomic.AtomicBoolean java. ...

  4. 死磕 java原子类之终结篇(面试题)

    概览 原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何线程上下文切换. 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割 ...

  5. 源码编译OpenJdk 8,Netbeans调试Java原子类在JVM中的实现(Ubuntu 16.04)

    一.前言 前一阵子比较好奇,想看到底层(虚拟机.汇编)怎么实现的java 并发那块. volatile是在汇编里加了lock前缀,因为volatile可以通过查看JIT编译器的汇编代码来看. 但是原子 ...

  6. Java 原子类 java.util.concurrent.atomic

    Java 原子类 java.util.concurrent.atomic 1.i++为什么是非线程安全的 i++其实是分为3个步骤:获取i的值, 把i+1, 把i+1的结果赋给i 如果多线程执行i++ ...

  7. java:原子类的CAS

    当一个处理器想要更新某个变量的值时,向总线发出LOCK#信号,此时其他处理器的对该变量的操作请求将被阻塞,发出锁定信号的处理器将独占共享内存,于是更新就是原子性的了. 1.compareAndSet- ...

  8. Java原子类实现原理分析

    在谈谈java中的volatile一文中,我们提到过并发包中的原子类可以解决类似num++这样的复合类操作的原子性问题,相比锁机制,使用原子类更精巧轻量,性能开销更小,本章就一起来分析下原子类的实现机 ...

  9. Java原子类及内部原理

    一.引入 原子是世界上的最小单位,具有不可分割性.比如 a=0:(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作.再比如:a++: 这个操作实际是a = a + ...

随机推荐

  1. .netcore+vue 实现压缩文件下载

    一.前言 目前接触的项目中,给定的需求是将系统内所有用户的数据整理好,并保存到文件夹内,目的主要是防止用户在实施人员已配置好的基础上由于不熟悉系统,导致的误删或者误操作.减少实施人员的配置工作.我首先 ...

  2. 栈帧的内部结构--局部变量表(Local Variables)

    每个栈帧中包含: 局部变量表(Local Variables) 操作数栈(Opreand Stack) 或表达式栈 动态链接 (Dynamic Linking) (或指向运行时常量的方法引用) 动态返 ...

  3. dubbo学习(五)注册中心zookeeper

    初识zookeeper 下载地址:https://archive.apache.org/dist/zookeeper/ 详细的ZooKeeper教程戳这里~ PS: 建议目前选择3.4的稳定版本进行使 ...

  4. Mac 环境下配置 MySQL 以及 Mac终端登录MySQL

    1.首先mysql官网下载Mac 版 mysql直接安装 2.打开偏好设置-> MySQL -> 查看是否开启mysql服务 3.打开终端 进入mysql目录: /usr/local/my ...

  5. Elasticsearch数据库 | Elasticsearch-7.5.0应用基础实战

    Elasticsearch 是一个可用于分布式以及符合RESTful 风格的搜索和数据分析引擎.-- Elastic Stack 官网 关于Elasticsearch的"爱恨情仇" ...

  6. Spring Boot第四弹,一文教你如何无感知切换日志框架?

    持续原创输出,点击上方蓝字关注我吧 目录 前言 Spring Boot 版本 什么是日志门面? 如何做到无感知切换? 如何切换? 引入依赖 指定配置文件 日志如何配置? 总结 前言 首先要感谢一下读者 ...

  7. 中秋国庆8天挑战赛 之 挑战8天掌握微信小程序

    中秋国庆8天挑战赛 挑战8天掌握微信小程序 当前学习进度: // 10.1​// 学习内容:​// 10.2​// 学习内容:​// 10.3​// 学习内容:​// 10.4​// 学习内容:​// ...

  8. 【题解】[SHOI2007]善意的投票 / [JLOI2010]冠军调查

    Link \(\text{Solution:}\) 我们令源点和汇点分别为睡觉和不睡觉这两种互斥的决策点.把小朋友看成点,问题转化为最小割. 每一个小朋友对自己的意愿指向的汇点/源点.容量为\(1.\ ...

  9. 【题解】[SDOI2010]捉迷藏

    题目链接:https://www.luogu.com.cn/problem/P2479 题目大意:求平面\(n\)个点中,到其它\(n-1\)个点的曼哈顿距离最大和最小距离之差最小的点,求出这个这个距 ...

  10. 插头 dp

    插头dp 洛谷 黑题板子? P5056 给出n×m的方格,有些格子不能铺线,其它格子必须铺,形成一个闭合回路.问有多少种铺法? 1.轮廓线 简单地说,轮廓线就是已决策格子和未决策格子的分界线: 2,插 ...