Java并发编程(四) CAS算法与atomic类

引言

前面的文章中提到并发编程需要保证操作的原子性、可见性和有序性。volatile关键字可以保证操作的可见性和有序性。而操作的原子性可以用synchronized关键字保证。但是如果在变量的读比较多,写比较少的时候使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

什么是CAS

比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。

入门实例

首先我们看一个简单的CAS实例:在程序中经常会出现先检查一个对象的值,然后再对这个值进行做一些操作

public class Lock {
    private boolean locked = false;
    public boolean lock() {
	 if(!locked) {
             locked = true;
             return true;
         }
	 return false;
    }
}

这样的操作在多线程时候肯定会出问题。我们可以用synchronized轻松的解决问题。

public class Lock {
    private boolean locked = false;
    public synchronized boolean lock() {
	 if(!locked) {
             locked = true;
             return true;
         }
	 return false;
    }
}

Java1.5以后,jdk提供java.util.concurrent.atomic包来提供原子操作。使用AtomicBoolean来实现:

public static class Lock {
    private AtomicBoolean locked = new AtomicBoolean(false);

    public boolean lock() {
        return locked.compareAndSet(false, true);
    }

}

atomic类

java.util.concurrent.atomic中的类可以分成4组:

标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
复合变量类:AtomicMarkableReference,AtomicStampedReference

一、标量类

AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference这四种基本类型用来处理布尔,整数,长整数,对象四种数据,其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法,从而避免了synchronized的高开销,执行效率大为提升。其实例各自提供对相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。

以AtomicLong为例

//构造方法
public AtomicLong(long initialValue)  //创建具有给定初始值的新 AtomicLong。
public AtomicLong()  //创建具有初始值 0 的新 AtomicLong。
//方法详细
public final long get()  //获取当前值。
public final void set(long newValue)  //设置为给定值。
public final void lazySet(long newValue)  //最后设置为给定值。1.6开始
public final long getAndSet(long newValue)   //以原子方式设置为给定值,并返回旧值。
//如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。参数:expect - 预期值   update - //新值返回:如果成功,则返回 true。返回 false 指示实际值与预期值不相等
public final boolean compareAndSet(long expect, long update) 
//如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。可能意外失败并且不提供排序保证,所以只能在很少的情况下对 compareAndSet 进行适当地选择。参数:expect - 预期值   update - 新值
//返回:如果成功,则返回 true。
public final boolean weakCompareAndSet(long expect,long update)
public final long getAndIncrement()  //以原子方式将当前值加 1。
public final long getAndDecrement()  //以原子方式将当前值减 1。
public final long getAndAdd(long delta)   //以原子方式将给定值添加到当前值,返回以前的值。
public final long incrementAndGet()   //以原子方式将当前值加 1。
public final long decrementAndGet()  // 以原子方式将当前值减 1。
public final long addAndGet(long delta)  //以原子方式将给定值添加到当前值,返回更新的值。
public String toString()   //返回当前值的字符串表示形式。
public int intValue()  //从类 Number 复制的描述,以 int 形式返回指定的数值。这可能会涉及到舍入或取整。转换为 int 类型后该对象表示的数值。
public long longValue() //从类 Number 复制的描述,以 long 形式返回指定的数值。这可能涉及到舍入或取整。指定者:类 Number 中的 longValue。返回:转换为 long 类型后该对象表示的数值。
public float floatValue() //从类 Number 复制的描述,以 float 形式返回指定的数值。这可能会涉及到舍入。指定者:类 Number 中的 floatValue,返回:转换为 float 类型后该对象表示的数值。
public double doubleValue() //从类 Number 复制的描述,以 double 形式返回指定的数值。这可能会涉及到舍入。指定者:类 Number 中的 doubleValue返回:转换为 double 类型后该对象表示的数值。

二、数组类
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray类进一步扩展了原子操作,对这些类型的数组提供了支持。这些类在为其数组元素提供volatile访问语义方面也引人注目,这对于普通数组来说是不受支持的。其内部并不是像AtomicInteger一样维持一个volatile变量,而是全部由native方法实现。数组变量进行volatile没有意义,因此set/get就需要unsafe来做了,但是多了一个index来指定操作数组中的哪一个元素。

三、更新器类
AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater和AtomicLongFieldUpdater 是基于反射的实用工具,可以提供对关联字段类型的访问,可用于获取任意选定volatile字段上的compareAndSet操作。它们主要用于原子数据结构中,该结构中同一节点的几个 volatile 字段都独立受原子更新控制。这些类在如何以及何时使用原子更新方面具有更大的灵活性,但相应的弊端是基于映射的设置较为拙笨、使用不太方便,而且在保证方面也较差。

四、复合变量类
AtomicMarkableReference 类将单个布尔值与引用关联起来。维护带有标记位的对象引用,可以原子方式更新带有标记位的引用类型。AtomicStampedReference 类将整数值与引用关联起来。维护带有整数“标志”的对象引用,可以原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和版本号,可以解决使用CAS进行原子更新时,可能出现的ABA问题。
 

ABA 问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
参考原文:https://blog.csdn.net/qq_34337272/article/details/81072874

CAS与synchronized的使用情景

简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)

对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
参考原文:https://blog.csdn.net/qq_34337272/article/details/81072874
 

猜你喜欢

转载自blog.csdn.net/qq_36154832/article/details/89476440