风在路上 风在路上
首页
导航站
  • Java-Se

    • Java基础
  • Java-Se进阶-多线程

    • 多线程
  • Java-Se进阶-java8新特性

    • java8新特性
  • Java-ee

    • JavaWeb
  • Java虚拟机

    • JVM
  • golang基础

    • golang基础
  • golang框架

    • gin
  • SQL 数据库

    • MySQL
  • NoSQL 数据库

    • Redis
    • ElasticSearch
    • MongoDB
  • ORM

    • MyBatis
    • MyBatis-Plus
  • Spring

    • Spring
  • SpringMVC

    • SpringMVC1
    • SpringMVC2
  • SpringCloud

    • SpringCloud
  • 中间件

    • RabbitMQ
    • Dubbo
  • 秒杀项目
  • Git
  • Linux
  • Docker
  • JWT
  • 面试
  • 刷题
开发问题😈
设计模式
关于💕
归档🕛
GitHub (opens new window)

风

摸鱼
首页
导航站
  • Java-Se

    • Java基础
  • Java-Se进阶-多线程

    • 多线程
  • Java-Se进阶-java8新特性

    • java8新特性
  • Java-ee

    • JavaWeb
  • Java虚拟机

    • JVM
  • golang基础

    • golang基础
  • golang框架

    • gin
  • SQL 数据库

    • MySQL
  • NoSQL 数据库

    • Redis
    • ElasticSearch
    • MongoDB
  • ORM

    • MyBatis
    • MyBatis-Plus
  • Spring

    • Spring
  • SpringMVC

    • SpringMVC1
    • SpringMVC2
  • SpringCloud

    • SpringCloud
  • 中间件

    • RabbitMQ
    • Dubbo
  • 秒杀项目
  • Git
  • Linux
  • Docker
  • JWT
  • 面试
  • 刷题
开发问题😈
设计模式
关于💕
归档🕛
GitHub (opens new window)
  • Java-基础

  • Java-多线程

    • 多线程
    • 多线程基础
    • 多线程进阶-JUC
    • 多线程进阶-JMM
    • 多线程进阶-volatile
    • 多线程进阶-单例模式升级
    • 多线程进阶-深入理解CAS
      • 6、深入理解CAS
        • Unsafe
        • CAS
        • CAS的优缺点
    • 多线程进阶-park和unpark
    • 多线程进阶-深入理解AQS
  • Java8新特性

  • JavaWeb

  • JVM

  • Java
  • Java-多线程
zdk
2022-01-06
目录

多线程进阶-深入理解CAS

Table of Contents generated with DocToc (opens new window)

  • 6、深入理解CAS
    • Unsafe
    • CAS
    • CAS的优缺点

# 6、深入理解CAS

# Unsafe

因为java无法直接访问底层操作系统,只能通过本地native方法来方法。不过尽管如此,JVM还是有一个后门:Unsafe类。

Unsafe类提供了硬件级别的原子操作。尽管这个类里面的方法都是public,但是并没有办法使用它们,JDK API文档也没有提供任何关于这个类中方法的介绍。总而言之,对于Unsafe类的使用都是受限的,只有授信的代码才能获得该类的实例,当然JDK库里面的类是可以随意使用的。因为如果操作Unsafe类的不是启动类加载器(Bootstrap),则抛出异常SecurityException("Unsafe")。

Unsafe提供了硬件级别的操作,比如说获取某个属性在内存中的位置,比如说修改对象的字段值,即使它是私有的。不过Java本身就是为了屏蔽底层的差异,对于一般的开发而言也很少会有这样的需求。

# CAS

CAS,Compare and Swap即比较并交换,设计并发算法时常用到的技术,java.util.concurrent包完全建立在CAS之上,没有CAS也就没有此包,可见CAS的重要性。当前的处理器基本都支持CAS,只不过不同的厂家实现不一样罢了。CAS有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做并返回false。

CAS也是通过Unsafe实现的,看下一下三个方法:

public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);
1
2
3
4
5

由CAS分析AtomicInteger原理,java.util.concurrent.atomic包下的原子操作类都是基于CAS实现的,下面先分析一下AtomicInteger类变量的定义:

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;
1
2
3
4
5
6
7
8
9
10

关于这段代码中出现的几个成员属性:

  1. Unsafe是CAS的核心类
  2. valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据原值的。
  3. 「关键」:value是用volatile修饰的

AtomicInteger中getAndIncrement是如何实现的,比如常用的addAndGet方法:

public final int addAndGet(int delta) {
    for (;;) {
        int current = get();
        int next = current + delta;
        if (compareAndSet(current, next))
            return next;
    }
}

public final int get() {
    return value;
}
1
2
3
4
5
6
7
8
9
10
11
12

这段代码如何在不加锁的情况下通过CAS实现线程安全:

  1. AtomicInteger中value原始值为3,即主内存中AtomicInteger的value为3,根据Java内存模型,线程1和线程2各自持有一份value的副本,值为3.
  2. 线程1运行到第三行获取到当前的value为3,线程切换。
  3. 线程2开始运行,获取value为3,利用CAS对比内存中的值也为3,比较成功,修改内存,此时内存中的value改变比如说4,线程切换。
  4. 线程1恢复运行,利用CAS比较发现自己的value为3,内存中的value为4,得到一个重要的结论->此时value正在被另外一个线程修改,所以我不能去修改。
  5. 线程1的compareAndSet失败,循环判断,因为value是volatile修饰的,所以它具备可见性的特性,线程2对于value的改变能被线程1看到,只要线程1发现当前获取的value是4,内存中的value也是4,说明线程2对于value的修改已经完毕并且线程1可以尝试去修改它。
  6. 最后说一点,比如说此时线程3也准备修改value了,因为比较-交换是一个原子操作不可被打断,线程3修改了value,线程1进行compareAndSet的时候必然返回false,这样线程1会继续循环去获取最新的value并进行compareAndSet,直至获取的value和内存中value一致为止。

整个过程中,利用CAS机制保证了对于value修改线程安全性。

# CAS的优缺点

  1. 优点

非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作。

  1. 缺点:
  1. CAS这种操作显然无法涵盖并发下的所有场景,并且CAS从语义上来说也不是完美的,存在这一一个逻辑漏洞:如果一个变量初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其它线程修改过了吗?如果这段期间它的值曾经被改成了B,然后又改回A,那么CAS操作就会误认为它从来没有被修改过。这个漏洞称为CAS操作的ABA问题。java.util.concurrent包为了解决这个问题,提供了一个带有标记性的原子引用类AtomicStampedRenference,它可以通过控制变量值的版本来保证CAS的正确性。不过目前来说这个类比较鸡肋,大部分情况下ABA问题并不会影响程序并发的正确性,如果需要解决ABA问题,使用传统的互斥同步可能会比原子类更加高效。
  2. 自旋时间过长,消耗CPU资源, 如果资源竞争激烈,多线程自旋长时间消耗资源。

AtomicStampedRenference解决ABA问题的demo

public class AtomicReferenceDemo {
    public static void main(String[] args) throws InstantiationException {
        AtomicStampedReference<String> atomicReference = new AtomicStampedReference<>("zdk",1);
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean update1 = atomicReference.compareAndSet("zdk", "zdk1", 1, 2);
            System.out.println("update1 = " + update1);
        }).start();
        new Thread(()->{
            boolean update2 = atomicReference.compareAndSet("zdk", "zdk2", 1, 2);
            System.out.println("update2 = " + update2);
        }).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在 GitHub 上编辑此页 (opens new window)
#多线程#JUC#CAS
最后更新: 2022/10/04, 16:10:00
多线程进阶-单例模式升级
多线程进阶-park和unpark

← 多线程进阶-单例模式升级 多线程进阶-park和unpark→

Theme by Vdoing | Copyright © 2022-2025 zdk | notes
湘ICP备2022001117号-1
川公网安备 51142102511562号
本网站由 提供CDN加速/云存储服务
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式