前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布

CAS

作者头像
胖虎
发布2020-12-08 14:47:00
8240
发布2020-12-08 14:47:00
举报
文章被收录于专栏:晏霖晏霖

曾经有人关注了我

后来他有了女朋友

在此部分讲解CAS概念是因为后面部分章节将会有很多地方使用到他,因为CAS是并发框架的基石,所以相当重要,读者需提前了解。本章节从概念、案例、源码浅析,一直到Java中一些典型的地方使用到CAS进行介绍。

2.5.1悲观锁和乐观锁

锁可以从不同角度去分类。Java中的锁有很多种,像悲观锁、乐观锁、自旋锁、适应性自旋锁、无锁、偏向锁、轻量级锁、重量级锁、公平锁、非公平锁、可重入锁、不可重入锁、共享锁、排他锁等,这还不包括各种所有锁的名词就已经有十多种了,对于这些名词的学习一般我们用模型去记忆,况且这些锁基本上望文生义,也是比较好理解的。本小结说明悲观锁、乐观锁,其他锁可在后续章节陆续讲解。

悲观锁和乐观锁是广义上的概念,体现了看到线程同步的不同角度。在Java中和数据库中都有此概念对应的应用。

悲观锁。顾名思义,他很悲观,它总是认为每次访问共享资源时都会发⽣冲突,所以一旦有线程获得到了锁,其他线程来访问,无论是读还是写他都是拒绝的,保证临界区的程序同⼀时间只能有⼀个线程在执⾏。例如synchronized关键字和Lock对象都是悲观锁。如图2-12所示。

图 2-12 悲观锁

乐观锁。也称“无锁”,顾名思义,他乐观了许多,他认为当一个线程进行使用数据时,其他线程不会来修改此数据,认为并发是“和谐的”,因此没有加锁。只是在更新数据的时候去判断在他之前有没有其他线程更改过此数据,如果他判断没有更新,则允许当前线程写入成功,如果他判断已经被其他线程更新过,就会根据不同实现方式进行不同操作(例如报错或者自动重试)。如图2-13所示。乐观锁在Java中是通过无锁的方式实现的,典型的应用就是CAS算法。

图2-13 乐观锁

根据从上面的概念描述我们可以发现:

l 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。

l 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

2.5.2 CAS概念

CAS(比较与交换,Compare and swap)是一种有名的无锁算法。

CAS指令需要有3个操作数,分别是内存为止(在Java中可以简单理解为变量的内存地址,用V表示)、旧的预期值(用E表示)和新值(用N表示)。CAS指令执行时,当且仅当V符合旧预期值E时,处理器用新值N更新V的值,否则他就不执行更新,但是无论是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作。

2.5.3 CAS在原子操作对象中的应用

下面的代码主要是使用了20个线程进行自增10000次来证明原子性。如代码2-9所示。

代码清单2-9 VolatileRight .java

代码语言:javascript
复制
@Slf4j
public class CASAtomicInteger {
    public static AtomicInteger race = new AtomicInteger(0);
    private static final int THREADS_COUNT = 20;

    public static void increase() {
        race.incrementAndGet();
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        log.info(race + "");
    }
}

运行结果是:20000。我们使用了AtomicInteger了,程序输出正确结果,一切都要归功于incrementAndGet()、getAndIncrement()方法的原子性,该方法无限循环,不断尝试将一个一个比当前值大1的新值赋给自己,如果失败了那说明在执行“获取-设置“操作的时候值已经有了修改,于是再次循环进行下一次操作,直到设置成功为止。

AtomicInteger实例中的incrementAndGet()、getAndIncrement()方法的区别很简单,可以用类似于++i和i++的赋值区别,我们来看一下源码,例2-10代码所示。

代码清单2-10 AtomicInteger.java

代码语言:javascript
复制
public final int getAndIncrement() {
    return U.getAndAddInt(this, VALUE, 1);
}
public final int incrementAndGet() {
    return U.getAndAddInt(this, VALUE, 1) + 1;
}

可以看出incrementAndGet()方法是getAndIncrement()返回值加1,所以我们得出结论。

incrementAndGet()方法以原子方式将当前值加1,并返回并返回新值(即加1后的值)。

getAndIncrement()方法以原子方式将当前值加1,并返回旧值(即加1前的原始值)。

2.5.4 compareAndSet源码浅析

在AtomicInteger实例中核心的方法就是compareAndSet()。2-11代码所示。

代码清单2-11 AtomicInteger.java

代码语言:javascript
复制
public final boolean compareAndSet(int expectedValue, int newValue) {
    return U.compareAndSetInt(this, VALUE, expectedValue, newValue);
}

首先,调用这个方法需要传递两个参数,一个是预期值,一个是新值,这个预期值就相当于数据库乐观锁版本号的概念,新值就是我们希望修改的值。

其实无论是原子递增、递减还是替换等方法都是调用compareAndSetInt()方法。但是compareAndSetInt()方法最终都会来到了unsafe下的compareAndSetInt()方法。代码如2-12所示。

代码清单2-12 Unsafe.java

代码语言:javascript
复制
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
                                             int expected,
                                             int x);

注意:笔者使用JDK11,上述代码中@HotSpotIntrinsicCandidate注解是Java 9引入的新特性,作用是JDK的源码中,被@HotSpotIntrinsicCandidate标注的方法,在HotSpot中都有一套高效的实现,该高效实现基于CPU指令,运行时,HotSpot维护的高效实现会替代JDK的源码实现,从而获得更高的效率。

读者可能会注意到compareAndSetInt()方法名字在jdk8之前的源码找不到,或者网上有些博客贴的代码是compareAndSwapInt()方法。其实这两个方法是一样的,原因在于JDK版本不同而已。

在调用compareAndSetInt()方法时,除了传递了我们传到此方法的两个参数之外,又传递了两个参数(Object o, long offset),这两个参数就是实例和偏移地址,this代表是当前类的实例,即AtomicInteger类的实例,这个offset就是确定我们需要修改的字段在实例的哪个位置。在 AtomicInteger类的实例中声明此静态变量,见代码清单2-13.我们使用他的构造方法创建AtomicInteger实例,其中value即代码清单2-9中new AtomicInteger(0);中的参数0,即AtomicInteger中的偏移地址。

代码清单2-13 AtomicInteger.java

代码语言:javascript
复制
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");

2.5.5 解决ABA问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说它值没有被其他线程改变过吗?

如果在这段期间它的值曾经改成了B,后来又改成了A,那么CAS操作就会误认为它没有改变过,这个漏洞称为“ABA”问题。J.U.C包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性,如果需要解决ABA问题,改用传统的互斥同步(典型的就是synchronized 和Lock)可能会比原子类更高效。

总结:Unsafe类是CAS实现的核心。 从名字可知,这个类标记为不安全的,CAS会使得程序设计比较负责,但是由于其优越的性能优势,以及天生免疫死锁(根本就没有锁,当然就不会有线程一直阻塞了),更为重要的是,使用无锁的方式没有所竞争带来的开销,也没有线程间频繁调度带来的开销,他比基于锁的方式有更优越的性能,所以在目前被广泛应用,我们在程序设计时也可以适当的使用.不过由于CAS编码确实稍微复杂,而且jdk作者本身也不希望你直接使用unsafe,所以如果不能深刻理解CAS以及unsafe还是要慎用,使用一些别人已经实现好的无锁类或者框架就好了。

2.5.6 CAS与单例模式

用CAS也可以完成单例模式,虽然在正常开发中,不会有人用CAS来完成单例模式,但是是检验是否学会CAS的一个很好的题目。例代码2-14。

代码清单 CASSingleton.java

代码语言:javascript
复制
public class CASSingleton {
    private CASSingleton() {
    }

    private static AtomicReference<CASSingleton> singletonAtomicReference = new AtomicReference<>();

    public static CASSingleton getInstance() {
        while (true) {
// 获得singleton
            CASSingleton singleton = singletonAtomicReference.get();            if (singleton != null) {// 如果singleton不为空,就返回singleton
                return singleton;
            }            // 如果singleton为空,创建一个singleton
            singleton = new CASSingleton();            // CAS操作,预期值是NULL,新值是singleton
            // 如果成功,返回singleton
            // 如果失败,进入第二次循环,singletonAtomicReference.get()就不会为空了
            if (singletonAtomicReference.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

2.5.7 JVM中的CAS应用

堆中对象的分配

简单的说new出来一个对象之前大小其实已经固定,把他放到堆里以什么形式储存的呢?

由于再给一个对象分配内存的时候不是原子性的操作,至少需要以下几步:查找空闲列表、分配内存、修改空闲列表等等,这是不安全的。解决并发时的安全问题也有两种策略:

1. CAS 。实际上虚拟机采用CAS配合上失败重试的方式保证更新操作的原子性,原理和上面讲的一样。

2. TLAB 。如果使用CAS其实对性能还是会有影响的,所以JVM又提出了一种更高级的优化策略:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部需要分配内存时直接在TLAB上分配就行,避免了线程冲突。只有当缓冲区的内存用光需要重新分配内存的时候才会进行CAS操作分配更大的内存空间。

虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来进行配置(jdk5及以后的版本默认是启用TLAB的)。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 晏霖 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档