玩转高并发系列----原子类型(一)

本文探讨了在多线程环境下,通过原子类型和CAS操作确保数据一致性的重要性。介绍了乐观锁与悲观锁的概念,深入剖析了Unsafe类的CAS操作及自旋与阻塞策略。详细解释了AtomicInteger、AtomicLong、AtomicReference等原子类的工作原理,以及AtomicStampedReference如何解决ABA问题。
原子类型的意义

对于一个共享变量,在多线程的情况下,我们需要对其进行同步操作,最常用的手段是添加synchronized关键字。如

public class SharedValue {
    private int value;
    public synchronized void add(int count){
        this.value += count;
    }

    public synchronized void sub(int count) {
        this.value -= count;
    }
}

但是,虽然synchronized关键字经过Java团队多年的优化(通过偏向锁->轻量级锁->重量级锁),其仍然存在一些性能问题,并且在开发中需要显示的添加synchronized关键字,容易遗漏某些方法。因此JDK团队在concurrent包中封装了很多的原子类型,底层通过CPU硬件级别的CAS锁保障了线程安全性。
2. 乐观锁和悲观锁

  1. 乐观锁:认为数据发生并发冲突的概率比较小,所以读操作之前不加锁。等到写操作的时候,再判断数据在此期间是否被其他线程修改了。如果被其他线程修改了,就把数据重新读出来,重复该过程。如果没有被修改,就写回去。判断数据是否被修改,同时还要写回新的值,这两个操作要合成一个原子操作,就是通过CPU提供的CAS实现的
  2. 悲观锁:任务数据发生并发冲突的概率很大,所以读操作之前就上锁。
  1. Unsafe类的CAS操作

上述的CAS操作,被JDK封装成了Unsafe类的一个native方法,供上层应用程序调用,如AtomicInteger

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

Unsafe类是整个Concurrent包的基础,里面所有函数都是native的,如上述的public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5)方法

  1. 第一个参数:是一个对象,表示的就是就是调用方对象。
  2. 第二个参数:需要特别说明,它是一个long型的整数,经常被称为xxxOffset,意思是某个成员变量在对应的类中的内存偏移量(该变量在内存中的地址),表示该成员变量本身。Unsafe有一个专门的函数,把成员变量转化为偏移量public native long objectFieldOffset(Field var1);
  3. 第三个参数:期望值,即希望xxxOffset处的变量的当前值,相等则更新该值为第四个参数,并返回true,不相等则返回false
  4. 第四个参数:新值,即等待被插入的值,插入不成功时,需通过”自旋“重新读取except(第三个参数)。

获取成员变量偏移量,即第二参数:如AtomicInteger中的static代码块

static {
    try {
      valueOffset = unsafe.objectFieldOffset
           (AtomicInteger.class.getDeclaredField("value"));
 	} catch (Exception ex) { throw new Error(ex); }
}
  1. 自旋与阻塞

当一个线程拿不到锁的时候,有以下两种基本的等待策略。

  1. 策略1:放弃CPU,进入阻塞状态,等待后续被唤醒,再重新被操作系统调度。也就是“阻塞”
  2. 策略2:不放弃CPU,空转,不断重试。也就是所谓的”自旋“
    注意:这两种策略并不是互斥的,可以结合使用。如果拿不到锁,先自旋几次;仍然拿不到锁时,进入阻塞。synchronized关键字就是这样实现的。

AtomicInteger&AtomicLong&AtomicReference
  1. AtomicInteger和AtomicLong都继承了Number抽象类,并且内部包装了一个value值(int类型或者long类型或者Object泛型类型),需要注意的是,value是volatile类型的。因此会保证多线程下,value值的可见性和有序性,避免重排序。
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
......
}
  1. 初始化一个Unsafe对象,后续的所有原子操作,都是通过Unsafe中的native方法完成的。
  2. 在静态代码块中初始化value的偏移量,后续unsafe操作中需要作为参数传入。
  3. 定义一个对应类型的volatile变量。作为共享变量。
  1. getAndIncrement以及incrementAndGet等方法都是都是通过unsafe.getAndAddXxx()完成,getAndSet是通过unsafe.getAndSet()方法完成。

AtomicBoolean
  1. 由于unsafe原生不支持对boolean的CAS操作,只提供了三种类型的CAS操作:int,long,Object,因此底层会将boolean类型,转换为int类型处理,true转换为1,false转换为0.
	private volatile int value;
......
    public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }

AtomicStampedReference&AtomicMarkableReference
  1. CAS引入的问题,即ABA问题:如果当前线程X执行compareAndSet方法时(此时已经取得了expect值A),另一个线程Y获取到CPU时间片,并且把变量的值从A改为B,然后再从B改到A,那么尽管修改过两次,可是当前线程做CAS操作的时候,却会因为值没变而认为数据没有被其他线程修改过。可以正常完成compareAndSet()操作。
  2. AtomicStampedReference如何解决ABA问题:比较时,不仅要比较“值”,而且提供一个版本号“stamp”,还要比较版本号是否一致。
  1. 提供了一个内部类Pair,封装了值value和版本号stamp.同时将Pair对象作为底层Unsafe的操作对象。
public class AtomicStampedReference<V> {
   private static class Pair<T> {
       final T reference;
       final int stamp;
       private Pair(T reference, int stamp) {
           this.reference = reference;
           this.stamp = stamp;
       }
       static <T> Pair<T> of(T reference, int stamp) {
           return new Pair<T>(reference, stamp);
       }
   }
   private volatile Pair<V> pair;
   ......
}
  1. 执行比较操作时,CAS操作compareAndSet()方法有四个参数,后面两个参数就是版本号的旧值和新值。
  • 当expectedReference != 对象当前的reference时,说明该数据肯定被其他线程修改过;直接返回false
    当expectedReference==对象当前的reference时,再进一步比较expectedStamp是否等于对象当前的版本号,以此判断数据是否被其他线程修改过。
public boolean compareAndSet(V   expectedReference,
                                V   newReference,
                                int expectedStamp,
                                int newStamp) {
       Pair<V> current = pair;
       return expectedReference == current.reference &&
           expectedStamp == current.stamp &&
           ((newReference == current.reference &&
             newStamp == current.stamp) ||
            casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
      return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
  1. 为什么没有AtomicStampedInteger或者AtomicStampedLong

因为这里要同时比较数据的“值”和“版本号”,而Integer或者Long的CAS没有办法同时比较两个变量,于是只能把值和版本号封装成一个对象。
扩展:后续章节将读写锁的时候,会提到JDK在实现读写锁时通过将一个int类型的CAS操作拆分成前16位和后16位,从而打完一次CAS完成多个操作的目的。

  1. AtomicMarkableReference:实现原理同AtomicStampedReference,不过将int类型的stamp版本号变成了一个boolean类型的标记。

此处需要注意的是:因为boolean类型的取值只有true和false两个版本号,所以并不能完全避免ABA问题,只是降低了ABA发生的概率。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值