[java] CAS介绍

Table of Contents

CAS基本概念介绍

CAS机制有什么作用

Java中的CAS

Unsafe类介绍

内存管理

非常规的对象实例化:

操作类、对象、变量

数组操作

多线程同步

挂起与恢复

内存屏障

CAS机制的问题

concurrent包的实现

Java9当中Unsafe类的变化

悲观锁和乐观锁定义,及其优缺点?


CAS基本概念介绍

CAS,它的全称是 compare and swap,直译成中文就是比较和交换的意思。缩写真是一个唬人的东西,尤其是对于不了解它的人们来说。

它既是一个理论也是一个操作。

一个CAS操作包含三个操作数—— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配(这句话需要仔细理解,什么叫内存位置的值与预期原值相匹配?),那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该等于值 A;如果等于该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。如果V的值不等于A,说明已经被其他线程修改了,当前线程可以放弃此操作,也可以再次尝试次操作直至修改成功。基于这样的算法,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰(临界区值的修改),并进行恰当的处理。

这里我们回答之前的问题:什么叫内存位置的值与预期原值相匹配?

答:内存位置 = V,预期原值 = A,在经过一系列操作之后计算获得B,那么我要不要把A变成B呢,在我计算出B之后,将A与V地址出的当前值做比较,若一样,则代表值尚未更改,则CAS操作成功。流程图如下:

值得指出的是:在最后一步B值计算出来写会V内存位置的时候,它怎么才能拿到当前V处最新的值呢,答案就是:volatile. volatile带来的可见性,让最新值是立即可得知的。关于volatile更多的内容请参考我的另一篇博客: [java] volatile关键字详解

CAS机制有什么作用

我们已经知道了CAS的工作原理,那么它的作用是什么呢。

主要是为了解决高并发的情况下,频繁加锁释放锁带来的性能问题。我们都知道在jdk1.5之前java语言是靠synchronized关键字保证同步的,而事实上在相当长的一段时间里synchronized的实现都是非常重量级的,会有较大的性能开销。

synchronized本质是一种独占锁,而独占锁事实上是一种悲观锁,会导致所有需要锁的线程挂起并等待锁释放被唤醒。而另一个更加有效的锁就是乐观锁。

所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap(还有别的机制吗?在别的博客上看到有人说通过加一个字段version,但从思想上讲加version和CAS是一样的思想,下面会提到)。有很多人也把这种锁称为无锁并发,因为乐观锁本质上是没有锁的,因此这种说法有一定道理。在数据库的锁机制当中,针对每一条数据的加锁,往往是采取在这条数据当中添加一个叫做version的字段,每次当数据有任何改动,则version增大一位,这在本质上与CAS的机制就是一样的,都是预期值和原来的值进行比较。只是CAS是从一个更底层的抽象上来描述的这个问题而已。

但是,CAS与通过version来判别又有一个非常重要的区别?是什么呢,就是对于version来说,他的修改是不可逆的,即version只会不停的变大而不会重复,而CAS的原值却存在一种被称作ABA的问题,就是原值原本是A,他被改成了B之后又改回了A。下面是一个这两者直间的逻辑示意图。在version变为3时,事实上有可能此时它对应的数据值与version为1时是一样的,但是没有关系,它们的version不一样。

一言以蔽之:CAS可以提高并发下的效率,它是一种乐观锁的机制。

另外,在阿里云的的java编码规范中有如下定义:

  • 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version作为更新依据。 说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次

Java中的CAS

在java中有一个叫做java.util.concurrent.atomic的包,该包下的所有类都是采用的CAS来实现的无锁,也就是乐观锁,乐观锁就是无锁。该包如下:

AtomicInteger类的实现:

private volatile int value;

//此处省略一万字代码

/**
 * Atomically sets to the given value and returns the old value.
 *
 * @param newValue the new value
 * @return the previous value
 */
public final int getAndSet(int newValue) {
    for (;;) {
        int current = get();
        if (compareAndSet(current, newValue))
            return current;
    }
}


/**
 * Atomically sets the value to the given updated value
 * if the current value {@code ==} the expected value.
 *
 * @param expect the expected value
 * @param update the new value
 * @return true if successful. False return indicates that
 * the actual value was not equal to the expected value.
 */
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

注意:value变量是被volatile修饰的。真正的赋值操作交给了Unsafe类来实现,Unsafe类中的方法几乎都是native方法。

Unsafe类介绍

在jdk1.8当中Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类,Java中CAS操作的执行依赖于Unsafe类的方法,很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。

Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。源码如下:

  @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

当然,知道反射的话,我们就可以知道任何事情都是可以办到的,可以使用反射按照如下方式获取一个unsafe类的实例

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

Unsafe类提供了以下这些功能:

内存管理

该部分包括了allocateMemory(分配内存)、reallocateMemory(重新分配内存)、copyMemory(拷贝内存)、freeMemory(释放内存 )、getAddress(获取内存地址)、addressSize、pageSize、getInt(获取内存地址指向的整数)、getIntVolatile(获取内存地址指向的整数,并支持volatile语义)、putInt(将整数写入指定内存地址)、putIntVolatile(将整数写入指定内存地址,并支持volatile语义)、putOrderedInt(将整数写入指定内存地址、有序或者有延迟的方法)等方法。getXXX和putXXX包含了各种基本类型的操作。

利用copyMemory方法,我们可以实现一个通用的对象拷贝方法,无需再对每一个对象都实现clone方法,当然这通用的方法只能做到对象浅拷贝。

非常规的对象实例化:

allocateInstance()方法提供了另一种创建实例的途径。通常我们可以用new或者反射来实例化对象,使用allocateInstance()方法可以直接生成对象实例,且无需调用构造方法和其它初始化方法。

这在对象反序列化的时候会很有用,能够重建和设置final字段,而不需要调用构造方法。

操作类、对象、变量

这部分包括了staticFieldOffset(静态域偏移)、defineClass(定义类)、defineAnonymousClass(定义匿名类)、ensureClassInitialized(确保类初始化)、objectFieldOffset(对象域偏移)等方法。

通过这些方法我们可以获取对象的指针,通过对指针进行偏移,我们不仅可以直接修改指针指向的数据(即使它们是私有的),甚至可以找到JVM已经认定为垃圾、可以进行回收的对象。

数组操作

这部分包括了arrayBaseOffset(获取数组第一个元素的偏移地址)、arrayIndexScale(获取数组中元素的增量地址)等方法。arrayBaseOffset与arrayIndexScale配合起来使用,就可以定位数组中每个元素在内存中的位置。

由于Java的数组最大值为Integer.MAX_VALUE,使用Unsafe类的内存分配方法可以实现超大数组。实际上这样的数据就可以认为是C数组,因此需要注意在合适的时间释放内存。

多线程同步

包括锁机制、CAS操作等。

这部分包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等方法。

其中monitorEnter、tryMonitorEnter、monitorExit已经被标记为deprecated,不建议使用。

Unsafe类的CAS操作可能是用的最多的,它为Java的锁机制提供了一种新的解决办法,比如AtomicInteger等类都是通过该方法来实现的。compareAndSwap方法是原子的,可以避免繁重的锁机制,提高代码效率。这是一种乐观锁,通常认为在大部分情况下不出现竞态条件,如果操作失败,会不断重试直到成功。

挂起与恢复

这部分包括了park、unpark等方法。

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。

内存屏障

这部分包括了loadFence、storeFence、fullFence等方法。这是在Java 8新引入的,用于定义内存屏障,避免代码重排序。

loadFence() 表示该方法之前的所有load操作在内存屏障之前完成。同理storeFence()表示该方法之前的所有store操作在内存屏障之前完成。fullFence()表示该方法之前的所有load、store操作在内存屏障之前完成。

p.s.此处参考:https://www.cnblogs.com/pkufork/p/java_unsafe.html

同时在JVM中的对象内存分配也使用到了CAS,当new一个对象时,我们需要为它分配一块内存,虚拟机是这样处理的:

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

虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来进行配置(jdk5及以后的版本默认是启用TLAB的)。另外上面解释了半天TLAB,其实TLAB的全称是Thread-local allocation buffer,一看到全称其实他的功能和思想就基本能猜到了。

CAS机制的问题

这个问题非常容易想到。CAS会将预期原值与当前位置的值比较,但是假如当前位置的值改了之后又改回去了怎么办呢?除此之外,还有循环时间长和只能保证一个共享变量的原子操作。

1.  ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号(在前面介绍数据库乐观锁时已经提到)。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

关于ABA问题参考文档: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

concurrent包的实现

由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

  1. A线程写volatile变量,随后B线程读这个volatile变量。
  2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。(注意,这里提到了线程间通信,我们已经知道的是线程间通信无非两种方式,第一共享变量,第二种等待通知,即wait/notify机制,这里显然是使用的共享变量的方式)

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:

Java9当中Unsafe类的变化

1.8之前Unsafe的包路径为:1.8之前Unsafe的包路径为。1.9中的路径为:package jdk.internal.misc;

1.8之前Unsafe是没有注释的,但是在java9中给予了非常详细的注释,

1.8之前Unsafe是不公开的类,只能通过反射或者使用系统类加载器使用,而到了java9,Unsafe包含了一个静态方法,可以直接拿到theUnsafe对象

悲观锁和乐观锁定义,及其优缺点?

参考:

https://blog.csdn.net/ls5718/article/details/52563959

https://blog.csdn.net/liubenlong007/article/details/53761730

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值