线程系列 5 - CAS 和 JUC原子类

线程系列 5 - CAS 和 JUC原子类

  • 1、关于 CAS
    • 1.1、啥是 CAS
    • 1.2、CAS的无锁编程
  • 2、关于 JUC 原子类
    • 2.1、基础原子类 AtomicInteger 为例
    • 2.2、数组原子类 AtomicIntegerArray 为例
    • 2.3、引用类型原子类 AtomicReference 为例
    • 2.4 属性更新原子类 AtomicIntegerFieldUpdater 为例
  • 3、解决ABA问题
    • 3.1、乐观锁解决(version)
    • 3.2、AtomicStampedReference 解决
    • 3.3、AtomicMarkableReference 解决
  • 4、总结CAS操作的弊端和规避措施
    • 4.1、 ABA问题
    • 4.2、开销问题
    • 4.3、只能保证一个共享变量之间的原子性操作

1、关于 CAS

 

1.1、啥是 CAS

 

       由于 JVM 的 Synchronized 重量级锁涉及操作系统(如Linux)内核态下互斥锁的使用,因此其线程阻塞和唤醒,都涉及进程在用户态到内核态的频繁切换,导致重量级锁开销大、性能低。而 JVM 的Synchronized 轻量级锁使用 CAS(Compare And Swap,比较并交换)进行自旋抢锁,CAS 是 CPU 指令级的原子操作,并处于用户态下,所以JVM轻量级锁的开销较小。
 
       JDK 5增加 JUC(java.util.concurrent)并发包,对操作系统的底层CAS原子操作进行了封装,为上层Java程序提供了 CAS 操作的API。
 

补充说明一下 用户态内核态

用户态执行的程序,进程所能访问的内存空间和对象受到限制,其所处于的cpu是可被抢占的。

内核态的进程,可以访问所有的内存空间和对象,且所占有的cpu是不允许被抢占的。

用户态进程可以通过【系统调用】主动要求切换到内核态,【中断】和【异常】也可导致两种状态间的切换。

 

       Unsafe 类的全限定名为sun.misc.Unsafe,Unsafe提供了 CAS 方法,Unsafe大量的方法都是native方法,基于C++语言实现,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。
 
Unsafe提供的CAS方法主要如下

 /**
 * 定义在Unsafe类中的三个“比较并交换”原子方法
 * @param o 需要操作的字段所在的对象
 * @param offset 需要操作的字段的偏移量(相对的,相对于对象头)
 * @param expected 期望值(旧的值)
 * @param update 更新值(新的值)
 * @return true 更新成功 | false 更新失败
 */
 public final native boolean compareAndSwapObject(
     Object o, long offset, Object expected, Object update);
 
 public final native boolean compareAndSwapInt(
     Object o, long offset, int expected,int update);
 
 public final native boolean compareAndSwapLong(
     Object o, long offset, long expected, long update);
     

 

1.2、CAS的无锁编程

 

       CAS是一种无锁算法,该算法关键依赖两个值:期望值(旧值)和新值。底层CPU利用原子操作判断内存原值与期望值是否相等,如果相等就给内存地址赋新值,否则不做任何操作。

       操作系统层面的 CAS 是一条 CPU 的原子指令(cmpxchg指令),正是由于该指令具备原子性,因此使用 CAS 操作数据时不会造成数据不一致的问题,Unsafe提供的CAS方法直接通过native方式(封装C++代码)调用了底层的CPU指令cmpxchg。

 

使用CAS进行无锁编程的步骤大致如下

  • ① 获得字段的期望值(oldValue)。

  • ② 计算出需要替换的新值(newValue)。

  • ③ 通过CAS将新值(newValue)放在字段的内存地址上,如果CAS失败就重复第 ① 步到第 ② 步,一直到CAS成功,这种重复俗称CAS自旋。

 

使用CAS进行无锁编程的伪代码如下:

 do{
    获得字段的期望值(oldValue);
    计算出需要替换的新值(newValue);
 } while (!CAS(内存地址,oldValue,newValue))

线程系列 5 - CAS 和 JUC原子类_第1张图片
 
       当CAS将内存地址的值与预期值进行比较时,如果相等,就证明内存地址的值没有被修改,可以替换成新值,然后继续往下运行;如果不相等,就说明内存地址的值已经被修改,放弃替换操作,然后重新自旋。当并发修改的线程少,冲突出现的机会少时,自旋的次数也会很少,CAS的性能会很高;当并发修改的线程多,冲突出现的机会多时,自旋的次数也会很多,CAS的性能会大大降低。所以,提升CAS无锁编程效率的关键在于减少冲突的机会。

 

2、关于 JUC 原子类

 

       使用 synchronized 的同步操作可以保证线程的安全性,但是会降低并发程序的性能。所以,JDK为线程不安全的操作提供了一些原子类,与 synchronized 同步机制相比,JDK原子类是基于CAS轻量级原子操作的实现,使得程序运行效率变得更高
 
       JUC并发包中的原子类存放在java.util.concurrent.atomic类路径下。根据操作的目标数据类型,可以将JUC包中的原子类分为4类:基本原子类数组原子类原子引用类字段更新原子类

① 基本原子类

  • AtomicInteger:整型原子类。
  • AtomicLong:长整型原子类。
  • AtomicBoolean:布尔型原子类。

② 数组原子类

  • AtomicIntegerArray:整型数组原子类。
  • AtomicLongArray:长整型数组原子类。
  • AtomicReferenceArray:引用类型数组原子类。

③ 引用原子类

  • AtomicReference:引用类型原子类。

  • AtomicMarkableReference:带有更新标记位的原子引用类型。

  • AtomicStampedReference:带有更新版本号的原子引用类型。

AtomicMarkableReference 类 将 boolean 标记与引用关联起来,可以 解决 使用 AtomicBoolean 进行原子更新时 可能出现的ABA问题

AtomicStampedReference 类 将整数值与引用关联起来,可以 解决 使用 AtomicInteger 进行原子更新时 可能出现的ABA问题

 

④ 字段更新原子类

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器。

  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。

  • AtomicReferenceFieldUpdater:原子更新引用类型中的字段。

 

2.1、基础原子类 AtomicInteger 为例

 

       在多线程环境下,如果涉及基本数据类型的并发操作,不建议采用synchronized重量级锁进行线程同步,而是建议优先使用基础原子类保障并发操作的线程安全性。

AtomicInteger、AtomicLong、AtomicBoolean三个基础原子类的方法几乎相同,我们以AtomicInteger为例详细说明。

常用方法:

 // 获取当前的值
 public final int get() 

 // 获取当前的值,然后设置新的值
 public final int getAndSet(int newValue) 

 // 获取当前的值,然后自增
 public final int getAndIncrement() 

 // 获取当前的值,然后自减
 public final int getAndDecrement() 

 // 获取当前的值,并加上预期的值
 public final int getAndAdd(int delta)

 //通过CAS方式设置整数值
 boolean compareAndSet(int expect, int update) 

代码示例:

import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class AtomicIntegerDemo {

    public static void main(String[] args) {
        final AtomicInteger atomic = new AtomicInteger(0);
        final AtomicInteger compareAndSetAtomic = new AtomicInteger(10);
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 5; i++) {
            pool.execute(()-> {
                log.info("getAndIncrement -> atomic = {}", atomic.getAndIncrement());
                // 获取当前的值
                log.info("get-1 -> atomic = {}", atomic.get());
            });
        }
        for (int i = 0; i < 5; i++) {
            pool.execute(()-> {
                log.info("getAndDecrement -> atomic = {}", atomic.getAndDecrement());
            });
        }
        for (int i = 0; i < 5; i++) {
            pool.execute(()-> {
                log.info("getAndSet -> atomic = {}", 
                              atomic.getAndSet(new Random().nextInt(10)));
            });
        }
        for (int i = 0; i < 5; i++) {
            pool.execute(()-> {
                // 通过CAS方式设置整数值 先比较再设值,如果当前值等于第一个参数,就设置为第二个值
                log.info("compareAndSet -> compareAndSetAtomic = {}", 
                                   compareAndSetAtomic.compareAndSet(10, 100));
                // 获取当前的值
                log.info("get-2 -> compareAndSetAtomic = {}", compareAndSetAtomic.get());
            });
        }
        pool.shutdown();
    }
}

 

AtomicInteger线程安全原理:

       基础原子类(以AtomicInteger为例)主要通过CAS自旋 + volatile 的方案实现,CAS用于保障变量操作的原子性,volatile关键字用于保障变量的可见性。采用这种方案,既保障了变量操作的线程安全性,又避免了synchronized重量级锁的高开销,使得Java程序的执行效率大为提升。

       AtomicInteger源码中的主要方法都是通过CAS自旋实现的。CAS自旋的主要操作为:如果一次CAS操作失败,获取最新的value值后,再次进行CAS操作,直到成功。

       AtomicInteger所包装的内部value成员,是一个使用关键字volatile修饰的内部成员。关键字volatile可以保证任何线程,在任何时刻总能拿到该变量的最新值,其目的在于保障变量值的线程可见性。

 

2.2、数组原子类 AtomicIntegerArray 为例

 

       数组原子类的功能是通过原子方式更数组中的某个元素的值。以 AtomicIntegerArray 类为例子,AtomicIntegerArray类的常用方法如下:

 

 //获取 index=i 位置元素的值
 public final int get(int i) 

 //返回 index=i 位置当前的值,并将其设置为新值:newValue
 public final int getAndSet(int i, int newValue)

 //获取 index=i 位置元素的值,并让该位置的元素自增
 public final int getAndIncrement(int i)

 //获取 index=i 位置元素的值,并让该位置的元素自减
 public final int getAndDecrement(int i) 

 //获取 index=i 位置元素的值,并加上预期的值
 public final int getAndAdd(int i, int delta) 
     
 //如果输入的数值等于预期值,就以原子方式将位置i的元素值设置为输入值getAndAdd
 boolean compareAndSet(int expect, int update) 

  //最终将位置i的元素设置为newValue
  //lazySet()方法可能导致其他线程在之后的一小段时间内还是可以读到旧的值
  public final void lazySet(int i, int newValue)

 

2.3、引用类型原子类 AtomicReference 为例

 
代码示例 2-3-1 :

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;

@Slf4j
public class AtomicReferenceDemo {

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(5);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        User user1 = new User("姓名1", 1);
        atomicReference.set(user1);
        log.info("初始 atomicReference -> get() -> {}", atomicReference.get());
        pool.execute(() -> {
            User user2 = new User("姓名2", 2);
            log.info("compareAndSet -> user2 -> {}", 
                          atomicReference.compareAndSet(user1, user2));
            log.info("user2 -> atomicReference -> get() -> {}", atomicReference.get());
        });
        pool.execute(() -> {
            User user3 = new User("姓名3", 3);
            log.info("compareAndSet -> user3 -> {}", 
                          atomicReference.compareAndSet(user1, user3));
            log.info("user3 -> atomicReference -> get() -> {}", atomicReference.get());
        });
        pool.shutdown();
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
class User {
    // 姓名
    private String name;
    // 年龄
    private Integer age;
}

打印结果 2-3-1 :

14:34:54.274 [main] INFO com.AtomicReferenceDemo - 初始 atomicReference -> get() -> User(name=姓名1, age=1)
14:34:54.278 [pool-1-thread-1] INFO com.AtomicReferenceDemo - compareAndSet -> user2 -> true
14:34:54.279 [pool-1-thread-2] INFO com.AtomicReferenceDemo - compareAndSet -> user3 -> false
14:34:54.279 [pool-1-thread-1] INFO com.AtomicReferenceDemo - user2 -> atomicReference -> get() -> User(name=姓名2, age=2)
14:34:54.279 [pool-1-thread-2] INFO com.AtomicReferenceDemo - user3 -> atomicReference -> get() -> User(name=姓名2, age=2)

Process finished with exit code 0

 

2.4 属性更新原子类 AtomicIntegerFieldUpdater 为例

 
如果需要保障对象某个字段(或者属性)更新操作的原子性,就需要用到属性更新原子类。

使用属性更新原子类保障属性安全更新的流程大致需要两步:

  • 第一步,更新的对象属性必须使用public volatile修饰符。

  • 第二步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须调用静态方法newUpdater() 创建一个更新器,并且需要设置想要更新的类和属性。

代码示例 2-4-1 :

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

@Slf4j
public class AtomicIntegerFieldUpdaterDemo {

    public static void main(String[] args) {
        // 调用静态方法newUpdater()创建一个更新器updater
        AtomicIntegerFieldUpdater<Dog> updater = 
                            AtomicIntegerFieldUpdater.newUpdater(Dog.class, "age");
        Dog dog = new Dog("旺财", "白色");
        log.info("init get -> {}", updater.get(dog));
        ExecutorService pool = Executors.newFixedThreadPool(5);
        pool.execute(() -> {
            //使用属性更新器的 getAndIncrement 增加 dog 的 age
            log.info("getAndIncrement -> {}", updater.getAndIncrement(dog));
        });
        pool.execute(() -> {
            //使用属性更新器的 getAndAdd 增加 dog 的 age
            log.info("getAndAdd -> {}", updater.getAndAdd(dog,3));
            log.info("getAndAdd -> get -> {}", updater.get(dog));
        });
        //使用属性更新器的get获取 dog 的age值
        log.info("get -> {}", updater.get(dog));
        pool.shutdown();
    }
}

class Dog {
    // 名字
    private String name;
    // 颜色
    private String colour;
    // 年龄
    public volatile int age;

    public Dog(String name, String colour) {
        this.name = name;
        this.colour = colour;
    }
}

打印结果 2-4-1 :

14:55:34.526 [main] INFO com.AtomicIntegerFieldUpdaterDemo - init get -> 0
14:55:34.533 [pool-1-thread-1] INFO com.AtomicIntegerFieldUpdaterDemo - getAndIncrement -> 0
14:55:34.533 [main] INFO com.AtomicIntegerFieldUpdaterDemo - get -> 1
14:55:34.533 [pool-1-thread-2] INFO com.AtomicIntegerFieldUpdaterDemo - getAndAdd -> 1
14:55:34.534 [pool-1-thread-2] INFO com.AtomicIntegerFieldUpdaterDemo - getAndAdd -> get -> 4

Process finished with exit code 0

 

3、解决ABA问题

 
       CAS原子操作使用不当就会存在ABA问题,举个例子说明ABA 问题:线程1从内存位置M中取出值A,另一个线程2也取出值A。现在假设线程2进行了一些操作之后将M位置的数据A变成了B,然后又在一些操作之后将B变成了A。这个时候线程1以为内存位置M的值A没被改过,但实际上被动过A->B->A.

 

3.1、乐观锁解决(version)

 

       解决ABA问题的常用方案,就是采用乐观锁使用版本号(version)方式处理。乐观锁每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加,不会减少。

 

3.2、AtomicStampedReference 解决

 

       JDK提供了一个AtomicStampedReference类来解决ABA问题。AtomicStampReference在CAS的基础上增加了一个Stamp(印戳或标记),使用这个印戳可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。

       AtomicStampReference 的 compareAndSet() 方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,就以原子方式将引用值和印戳(Stamp)标志的值更新为给定的更新值。

 // 构造函数,V表示要引用的原始数据,initialStamp表示最初的版本印戳(版本号)
 AtomicStampedReference(V initialRef, int initialStamp)
 
 /******** 常用方法: *******/
 
 //获取被封装的数据
 public V getRerference();
 
 //获取被封装的数据的版本印戳
 public int getStamp();
 
 /**
  * @param expectedReference 预期引用值
  * @param newReference      更新后的引用值
  * @param expectedStamp     预期印戳 (Stamp)标志值
  * @param newStamp          更新后的印戳(Stamp)标志值
  **/
 public boolean compareAndSet(V expectedReference, V newReference, 
             int expectedStamp, int newStamp);

代码示例 3-2-1 :

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

@Slf4j
public class AtomicStampedReferenceDemo {

    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool(5);
        Mail mail1 = new Mail("测试邮件", 1);
        AtomicStampedReference<Mail> stampedReference = 
                                    new AtomicStampedReference<>(mail1, 1);
        pool.execute(() -> {
            // 获取当前版本号
            int stamp = stampedReference.getStamp();
            log.info("测试邮件->状态2 sleep 前,版本号={},mail={}",stamp,
                                    stampedReference.getReference());
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Mail mail2 = new Mail("测试邮件", 2);
            Boolean result = stampedReference.compareAndSet(mail1, mail2, stamp, stamp + 1);
            log.info("测试邮件->状态2 -> 执行情况 = {}", result);
            stamp++;
            log.info("此刻的版本号是->{}", stampedReference.getStamp());
            Boolean resultToo = 
              stampedReference.compareAndSet(mail2, new Mail("测试邮件", 3), stamp, stamp + 1);
            log.info("测试邮件->状态3 -> 执行情况 = {}", resultToo);
        });

        pool.execute(() -> {
            // 获取当前版本号
            int stamp = stampedReference.getStamp();
            log.info("测试邮件->状态4 sleep 前,版本号={},mail={}",stamp,  
                                         stampedReference.getReference());
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Boolean result = 
              stampedReference.compareAndSet(mail1, new Mail("测试邮件", 4), stamp, stamp + 1);
            log.info("测试邮件->状态4 -> 执行情况 = {}", result);
        });
        pool.shutdown();
    }

}

@Data
@NoArgsConstructor
@AllArgsConstructor
class Mail {
    // 主题
    private String subject;
    // 状态
    private Integer status;
}

执行结果 3-2-1 :

15:46:25.822 [pool-1-thread-2] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态4 sleep 前,版本号=1,mail=Mail(subject=测试邮件, status=1)
15:46:25.822 [pool-1-thread-1] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态2 sleep 前,版本号=1,mail=Mail(subject=测试邮件, status=1)
15:46:30.837 [pool-1-thread-2] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态4 -> 执行情况 = false
15:46:30.837 [pool-1-thread-1] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态2 -> 执行情况 = true
15:46:30.837 [pool-1-thread-1] INFO com.AtomicStampedReferenceDemo - 此刻的版本号是->2
15:46:30.838 [pool-1-thread-1] INFO com.AtomicStampedReferenceDemo - 测试邮件->状态3 -> 执行情况 = true

Process finished with exit code 0

 

3.3、AtomicMarkableReference 解决

 
AtomicMarkableReference适用于只要知道对象是否被修改过,而不适用于对象被反复修改的场景。

代码示例 3-3-1 :

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicMarkableReference;

@Slf4j
public class AtomicMarkableReferenceDemo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(5);
        AtomicMarkableReference<Integer> markableReference =
                new AtomicMarkableReference<Integer>(1, false);
        pool.execute(() -> {
            Integer reference = markableReference.getReference();
            Boolean mark = markableReference.isMarked();
            log.info("mark={}, reference={}", mark, reference);
            markableReference.compareAndSet(reference, 12, mark, !mark);

            Boolean newMark1 = markableReference.isMarked();
            log.info("newMark1={}, reference={}", newMark1, 
                                       markableReference.getReference());
        });

        pool.execute(() -> {
            Integer reference = markableReference.getReference();
            Boolean mark_ = markableReference.isMarked();
            log.info("mark_ ={}, reference_={}", mark_, reference);
            markableReference.compareAndSet(reference, 12, mark_, !mark_);

            Boolean newMark2 = markableReference.isMarked();
            log.info("newMark2_={}, reference_={}", newMark2, 
                                        markableReference.getReference());
        });
        pool.shutdown();
    }
    
}

执行结果 3-3-1 :

16:19:35.120 [pool-1-thread-2] INFO com.AtomicMarkableReferenceDemo - mark_ =false, reference_=1
16:19:35.120 [pool-1-thread-1] INFO com.AtomicMarkableReferenceDemo - mark=false, reference=1
16:19:35.124 [pool-1-thread-1] INFO com.AtomicMarkableReferenceDemo - newMark1=true, reference=12
16:19:35.124 [pool-1-thread-2] INFO com.AtomicMarkableReferenceDemo - newMark2_=true, reference_=12

Process finished with exit code 0

 

4、总结CAS操作的弊端和规避措施

 

CAS操作的弊端主要有三点:ABA问题开销问题只能保证一个共享变量之间的原子性操作。

 

4.1、 ABA问题

 

  • ① ABA问题的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候将版本号加1,那么操作序列A==>B==>A就会变成 A1==>B2==>A3,如果将A1当作A3的预期数据,就会操作失败。

  • ② AtomicStampedReference

  • ③ AtomicMarkableReference

 

4.2、开销问题

 

       自旋CAS如果长时间不成功(不成功就一直循环执行,直到成功),就会给CPU带来非常大的执行开销。
 
解决CAS恶性空自旋的有效方式之一是以空间换时间,较为常见的方案为:

  • ① 分散操作热点,使用LongAdder替代基础原子类 AtomicLong,LongAdder将单个CAS热点(value值)分散到一个cells 数组中。

  • ② 使用队列削峰,将发生CAS争用的线程加入一个队列中排队,降低CAS争用的激烈程度。JUC中非常重要的基础类AQS(抽象队列同步器)就是这么做的。

4.3、只能保证一个共享变量之间的原子性操作

 

       当对一个共享变量执行操作时,可以使用CAS的方式来保证原子操作,但对多个共享变量操作时,CAS就无法保证操作的原子性。常把多个共享变量合并成一个共享变量来操作,来规避该问题。AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个AtomicReference实例后再进行CAS操作。
 

 

 
 
 
 
 
.

你可能感兴趣的:(线程系列,CAS,JUC,AtomicInteger,AtomicReference,ABA,并发编程)