栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

juc-06-CAS与Atomic原子类

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

juc-06-CAS与Atomic原子类

这篇文章,说说 CAS,还有Java java.util.concurrent.atomic 包下的 Atomic 原子类。

1、什么是CAS

CAS:Compare and Swap,即比较再交换。cpu的特殊指令,指令级别保证这是一个原子操作。
CAS 需要有3个操作数:内存值V,旧的预期值A,即将要更新的目标值B。 CAS指令执行时,当且仅当内存值V的值与预期值A相等时,将内存值V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

通常在自旋(死循环)中不断地进行 CAS 操作。

2、模仿 CAS

public class MockCas {

    
    private volatile int value;

    public MockCas(int value) {
 this.value = value;
    }

    
    public synchronized int compareAndSwap(int expectedValue, int newValue) {
 int oldValue = value;
 if (oldValue == expectedValue) {
     value = newValue;
     System.out.println(Thread.currentThread().getName() + " cas 操作成功");
 } else {
     System.out.println(Thread.currentThread().getName() + " cas 操作失败");
 }
 return oldValue;
    }

    public static void main(String[] args) throws InterruptedException {
 // 内存值是 100
 int value = 100;
 int expectedValue = 100;
 MockCas cas = new MockCas(value);
 Thread thread0 = new Thread(() -> {
     int oldValue = cas.compareAndSwap(expectedValue, 101);
     System.out.println(Thread.currentThread().getName() + " cas 内存值:" + oldValue);
 });
 Thread thread1 = new Thread(() -> {
     int oldValue = cas.compareAndSwap(expectedValue, 101);
     System.out.println(Thread.currentThread().getName() + " cas 内存值:" + oldValue);
 });
 thread0.start();
 thread1.start();
 thread0.join();
 thread1.join();
    }
}

运行结果:

Thread-0 cas 操作成功
Thread-0 cas 内存值:100
Thread-1 cas 操作失败
Thread-1 cas 内存值:101
3、Java中原子类

原子类的作用和锁类似,是为了保证并发情况下线程安全。
不过原子类相比于锁,有一定的优势
1)粒度更细:原子变量可以把竞争范围缩小到变量级别,这是我们可以获得的最细粒度的情况了,通常锁的粒度都要大于原子变量的粒度
2)效率更高:通常,使用原子类的效率会比使用锁的效率更高。

Jdk java.util.concurrent.atomic 包中提供了 6 类原子类

  • Atomic* 基本类型原子类:AtomicBoolean,AtomicInteger,AtomicLong
  • Atomic*Array 数组类型原子类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • Atomic*Reference 引用类型原子类:AtomicReference,AtomicMarkableReference,AtomicStampedReference
  • Atomic*FieldUpdater 升级原子类: AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
  • Adder累加器:LongAdder、DoubleAdder
  • Accumulator累加器:LongAccumulator、DoubleAccumulator
4、Java 是如何利用 CAS 实现原子操作的

原子操作,是不可分割,不可中断的,即便是多线程的情况下也可以保证。

Unsafe类

Unsafe 是CAS的核心类。Java无法直接访问低层操作系统,而是通过本地方法(native)来进行访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类 Unsafe ,它提供了硬件级别的原子操作。

valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的,就这样我们就能通过 Unsafe 来实现CAS了。

以 AtomicInteger 为例,分析在Java中是如何利用 CAS 实现原子操作的?
AtomicInteger 加载 Unsafe 工具,用来直接操作内存数据,用 Unsafe 来实现低层 CAS 操作, 用 volatile 修饰 value 字段,保证可见性。

AtomicInteger getAndAdd()方法 源码

// AtomicInteger 部分源码
public class AtomicInteger extends Number implements java.io.Serializable {
...
// setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
 try {
     // value 的内存偏移地址
     valueOffset = unsafe.objectFieldOffset
  (AtomicInteger.class.getDeclaredField("value"));
 } catch (Exception ex) { throw new Error(ex); }
    }

    // value值
    private volatile int value;

    
    public final int getAndAdd(int delta) {
 // unsafe 中 do-while 循环,自旋,直至 cas 操作成功
 return unsafe.getAndAddInt(this, valueOffset, delta);
    }
...
}

// Unsafe部分源码 getAndAddInt
public final class Unsafe {

...
    public final int getAndAddInt(Object var1, long var2, int var4) {
 // do-while 循环,自旋,不断地调用 compareAndSwapInt(cas操作) 直到成功
 int var5;
 do {
     var5 = this.getIntVolatile(var1, var2);
 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

 return var5;
    }

    // 低层的 cas 操作
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

...
}
5、基础类型原子类,演示AtomicInteger的基本用法

对比非原子类的线程安全问题,使用了原子类之后,不需要加锁,也可以保证线程安全。

AtomicInteger 常用API:

  1. public final int get():获取当前的值
  2. public final int getAndSet(int newValue):获取当前的值,并设置新的值
  3. public final int getAndIncrement():获取当前的值,并自增1
  4. public final int getAndDecrement():获取当前值,并自减1
  5. public final int getAndAdd(int delta):获取当前的值,并加上预期的值
  6. public final boolean compareAndSet(int expect, int update):如果输入的等于预期值,则以原子方式将该值设置为输入值,设置成功返回true,否则 expect 不等于 内存值,返回false


public class AtomicIntegerDemo implements Runnable {

    
    private static volatile int var = 0;

    
    private static final AtomicInteger atomicInteger = new AtomicInteger();

    
    public void incrementBasic() {
 var++;
    }

    
    public void incrementAtomic() {
 atomicInteger.getAndIncrement();
    }

    @Override
    public void run() {
 for (int i = 0; i < 10000; i++) {
     incrementAtomic();
     incrementBasic();
 }
    }

    public static void main(String[] args) throws InterruptedException {
 AtomicIntegerDemo r = new AtomicIntegerDemo();
 Thread t1 = new Thread(r);
 Thread t2 = new Thread(r);
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 System.out.println("原子类的结果:" + atomicInteger.get());
 System.out.println("普通变量的结果:" + var);
    }
}

运行结果:

原子类的结果:20000
普通变量的结果:19264

2个线程,分别进行10000次 +1 操作,可以看出原子变量的结果是符合预期的,而普通变量的结果则是不正确的。

6、数组类型原子类,演示 AtomicIntegerArray 的基本用法
  • AtomicIntegerArray(int length):根据给定的 length 初始化数组,新建长度为 length 的成员变量array,array = new int[length];
  • AtomicIntegerArray(int[] array):根据给定的数组 clone() 初始化成员变量的 array,this.array = array.clone();
  • int addAndGet(int i, int delta):对数组中下标为 i 的元素添加 delta 并返回更新后的值
  • boolean compareAndSet(int i, int expect, int update):对数组中下标为 i 的元素传入 expect 值和 update 值,进行 cas 操作,如果 expect 值等于内存值,则修改成功,返回true,否则返回false
  • int decrementAndGet(int i):对数组中下标为 i 的元素 -1,然后返回修改后的值
  • int get(int i):返回数组中下标为 i 的元素的值
  • int getAndAdd(int i, int delta):对数组中下标为 i 的元素添加 delta 并返回更新前的值
  • int getAndDecrement(int i):对数组中下标为 i 的元素 -1,然后返回修改前的值
  • int getAndIncrement(int i):对数组中下标为 i 的元素 +1,然后返回修改前的值
  • int getAndSet(int i, int newValue):对数组中下标为 i 的元素自旋设值 newValue,然后返回修改前的值
  • int incrementAndGet(int i):对数组中下标为 i 的元素 +1,然后返回修改后的值
  • int length():返回数组的长度

两个线程并发对长度为 5 的普通数组变量和原子类 AtomicIntegerArray 的每个元素分别进行10000次+1,原子变量的结果是符合预期的,而普通数组变量的结果则是不正确的。


public class AtomicArrayDemo implements Runnable {

    static int length = 5;
    
    private static volatile int[] array = new int[length];

    
    private static final AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(length);

    
    public void incrementBasic(int index) {
 array[index]++;
    }

    
    public void incrementAtomic(int index) {
 atomicIntegerArray.getAndIncrement(index);
    }

    @Override
    public void run() {
 for (int index = 0; index < length; index++) {
     for (int count = 0; count < 10000; count++) {
  incrementAtomic(index);
  incrementBasic(index);
     }
 }
    }


    public static void main(String[] args) throws InterruptedException {
 AtomicArrayDemo r = new AtomicArrayDemo();
 Thread t1 = new Thread(r);
 Thread t2 = new Thread(r);
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 System.out.println("原子类的结果:" + atomicIntegerArray);
 System.out.println("普通变量的结果:");
 for (int item : array) {
     System.out.print(item + " ");
 }
    }
}

运行结果:

原子类的结果:[20000, 20000, 20000, 20000, 20000]
普通变量的结果:
19345 18226 17153 17765 19513 
7、引用类型原子类,演示 AtomicReference 的基本用法
  • V get():获取 AtomicReference 的 value ,是一个引用类型。
  • set(V newValue):设置 AtomicReference 的 新值
  • V getAndSet(V newValue):设置 AtomicReference 的 新值,并返回旧的值

public class AtomicReferenceDemo {
	
	static AtomicReference userRef = new AtomicReference();
	
    public static void main(String[] args) {
 UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
 userRef.set(user);
 
 UserInfo updateUser = new UserInfo("Bill", 17);//要变化的新实例
 userRef.compareAndSet(user, updateUser);
 System.out.println(userRef.get().getName());
 System.out.println(userRef.get().getAge());
 System.out.println(user.getName());
 System.out.println(user.getAge()); 
    }
    
    //定义一个实体类
    static class UserInfo {
 private String name;
 private int age;
 public UserInfo(String name, int age) {
     this.name = name;
     this.age = age;
 }
 public String getName() {
     return name;
 }
 public int getAge() {
     return age;
 }
    }

}

运行结果:

Bill
17
Mark
15
8、带版本戳引用类型原子类,演示 AtomicStampedReference 的基本用法


public class AtomicStampedReferenceDemo {

    static AtomicStampedReference asr =
     new AtomicStampedReference<>("Xander", 0);


    public static void main(String[] args) throws InterruptedException {
 final int oldStamp = asr.getStamp();//拿初始的版本号
 final String oldReferenc = asr.getReference();//拿初始的引用

 System.out.println(oldReferenc + "===========" + oldStamp);

 Thread rightStampThread = new Thread(new Runnable() {

     @Override
     public void run() {
  String reference = asr.getReference();
  System.out.println(Thread.currentThread().getName()
   + "  修改前变量值:" + reference + "  修改前版本戳:" + asr.getStamp() + "  修改成功:"
   + asr.compareAndSet(oldReferenc, oldReferenc + "Java", oldStamp, oldStamp + 1));
     }

 });

 Thread errorStampThread = new Thread(new Runnable() {

     @Override
     public void run() {
  String reference = asr.getReference();
  System.out.println(Thread.currentThread().getName()
   + "  修改前变量值:" + reference + "  修改前版本戳:" + asr.getStamp() + "  修改成功:"
   + asr.compareAndSet(reference, reference + "C", oldStamp, oldStamp + 1));

     }

 });

 rightStampThread.start();
 rightStampThread.join();
 errorStampThread.start();
 errorStampThread.join();
 System.out.println(asr.getReference() + "===========" + asr.getStamp());

    }
}

运行结果:

Xander===========0
Thread-0  修改前变量值:Xander  修改前版本戳:0  修改成功:true
Thread-1  修改前变量值:XanderJava  修改前版本戳:1  修改成功:false
XanderJava===========1
9、CAS的问题 ABA 问题

CAS 的理念是,在我修改值期间,没有其他人进行修改,我最后再修改值。

什么是 ABA 问题?
①线程1和线程2,同时需要对内存值V进行操作,同时读出当前内存值V=A,
②线程1 准备修改为 A1 ,于是它准备执行 compareAndSwap(int expectedValue, int newValue),其中 expectedValue=A,newValue=A1;
③在线程1准备进行修改之前,线程2也对内存值V 进行修改,并且进行了两次修改,先从 A 修改为 B (compareAndSwap(A,B)),再由 B 修改为 A (compareAndSwap(B,A));
④线程1最后 compareAndSwap(A, A1),这时候,线程1也能够执行成功,但是会存在很多潜在的问题,因为在线程1修改期间,内存值V已经被多次修改。

怎么避免 ABA 问题?可以通过版本号解决,例如原子类AtomicStampedReference。

开销问题

通常原子操作是在自旋(死循环)中不断地进行 CAS 操作直至成功,例如 Unsafe 类中的 getAndAddInt()。
如果我们设计类似 Unsafe 类中的自旋算法时,需要注意如果 CAS 操作长期不成功,cpu不断的循环,会存在开销问题。

// Unsafe部分源码 getAndAddInt
public final class Unsafe {

...
    public final int getAndAddInt(Object var1, long var2, int var4) {
 // do-while 循环,自旋,不断地调用 compareAndSwapInt(cas操作) 直到成功
 int var5;
 do {
     var5 = this.getIntVolatile(var1, var2);
 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

 return var5;
    }

    // 低层的 cas 操作
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

...
}

这篇文章给大家浅谈了什么是 CAS,给大家介绍了 JDK 中常用类型的 Atomic* 原子类的使用,我这里也是简单的介绍,相信大家读完这篇文章,理解 CAS 算法后,再去读其他原子类的源码,也是很简单的,最后说明了 CAS算法和自旋,可能会存在的 ABA问题和开销问题。

代码:
https://github.com/wengxingxia/002juc.git

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号