并发编程学习笔记 之 原子操作类AtomicInteger详解

导读:本篇文章讲解 并发编程学习笔记 之 原子操作类AtomicInteger详解,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、直观体验

  原子类型就是一种无锁的、线程安全的类型,解决了在多线程场景下使用基本数据类型和引用类型的线程不安全的问题。具体示例如下:

第一种场景:模拟多个线程同时执行,实现计数器的自增
public class AtomicIntegerTest {

    private static final int THREADS_CONUT = 10;
    public static int count = 0;

    public static void increase() {
        count++;
    }
    public static void main(String[] args) {
        for(int i = 0; i < THREADS_CONUT; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //为了避免线程快速执行完成,变成了类似串行执行的效果
                        TimeUnit.SECONDS.sleep(new Random().nextInt(3));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int i = 0; i < 1000; i++) {
                        increase();
                    }
                }
            });
            t.start();
        }
        while (Thread.activeCount() > 2) {//idea开发工具需要大于2
            Thread.yield();
        }
        System.out.println("count:" + count);
    }
}

  在上述代码中,运行了10个线程,每个线程均对count变量进行了1000的计数器自增操作,理想情况下,运行结束后,这个时候count的值应该是10000,但是实际情况,每次结果都可能不相同,有可能是10000,也有可能是小于10000的其他值。为什么出现这种情况呢?其实就是多个线程对count共享变量进行并行操作,导致部分线程的计算结果对其他线程不可见,造成了数据丢失。那么在共享变了添加volatile关键字,能否解决上述问题呢?

  上述代码,添加volatile关键字即可,如下所示:

public static volatile int count = 0;

  执行了上述添加volatile关键字的代码后,继续执行代码,还是无法保证每次结果都是10000,但是好像和不添加有一些变化,出现10000的几率变高了,而且结果更加解决10000了(手动运行代码得出的结论,可能存在偏差)。为什么还是无法保证结果的正确性呢?为什么结果更加接近10000了呢?这里其实就是因为volatile 关键字的特性决定的。

volatile 关键字的特性:

  • 保证变量的内存可见性。保证共享变量在线程之间的可见性,即对volatile变量所有的写操作都能立即反应到其他线程中
  • 局部阻止重排序的发生。具体后续会专门学习volatile 关键字的用法。

  因为volatile 保证了有序性和可见性,但是无法保证变量的原子性,所以还是无法得到预期的结果,归根结底就是没有保证对共享变量修改操作的原子性操作。

  我们知道synchronized关键字可以实现变量或代码块的原子性操作,我们如果在increase()方法上,使用synchronized关键字修饰,这个时候,我们在运行,可以发现每次的结果都是10000了,说明保证了共享变量变更操作的原子性,就可以保证多线程环境下,结果的正确性了。

 public synchronized static void increase() {
     count++;
 }
第二种场景:使用原子操作类AtomicInteger,实现计数器的自增

  在前面我们尝试使用了volatile 或 synchronized 关键字,保证int类型的共享变量的原子性操作,那么是否有其他更加简单和高效的方式呢?答案是肯定的,那就是咱们今天的主角“AtomicInteger”,AtomicInteger是原子类型,它是一种无锁的、线程安全的类型,解决了在多线程场景下线程不安全的问题。

  使用原子操作类AtomicInteger的例子和上述代码类似,只需要修改定义共享变量类型和自增方法即可,这个时候,我们不在需要volatile 或 synchronized 关键字,即可解决多线程场景下的线程不安全的问题,如下所示:

public static AtomicInteger count = new AtomicInteger(0);

public static void increase() {
    count.incrementAndGet();
}

二、深入学习

  我们现在开始学习关于原子操作类AtomicInteger的常见用法。与int的引用类型Integer继承Number类一样,AtomicInteger也是Number类的一个子类,除此之外,AtomicInteger还提供了很多原子性的操作方法。在AtomicInteger的内部有一个被volatile关键字修饰的成员变量value,实际上,AtomicInteger所提供的所有方法主要都是针对该变量value进行的操作。

1、AtomicInteger的构造函数
  • public AtomicInteger():创建AtomicInteger的初始值为0。
  • public AtomicInteger(int initialValue):创建AtomicInteger并且指定初始值,无参的AtomicInteger对象创建等价于AtomicInteger(0)。
2、AtomicInteger的Incremental操作
  • int getAndIncrement():返回当前int类型的value值,然后对value进行自增运算,该操作方法能够确保对value的原子性增量操作。
  • int incrementAndGet():直接返回自增后的结果,该操作方法能够确保对value的原子性增量操作。
3、AtomicInteger的Decremental操作
  • int getAndDecrement():返回当前int类型的value值,然后对value进行自减运算,该操作方法能够确保对value的原子性减量操作。
  • int decrementAndGet():直接返回自减后的结果,该操作方法能够确保对value的原子性减量操作。
4、AtomicInteger的原子性更新操作
  • boolean compareAndSet(int expect, int update):原子性地更新AtomicInteger的值,其中expect代表当前的AtomicInteger数值,update则是需要设置的新值,该方法会返回一个boolean的结果:当expect和AtomicInteger的当前值不相等时,修改会失败,返回值为false;若修改成功则会返回true。
  • boolean weakCompareAndSet(int expect, int update):目前版本JDK中的该方法与compareAndSet完全一样。(现在两个方法的实现完全一样,其实在JDK 1.6版本以前双方的实现是存在差异的)
  • int getAndAdd(int delta):原子性地更新AtomicInteger 的value值,更新后的value为value和delta之和,方法的返回值为value的前一个值,该方法实际上是基于自旋+CAS算法实现的(Compare And Swap)原子性操作。
  • int addAndGet(int delta):该方法与getAndAdd(int delta)一样,也是原子性地更新AtomicInteger的value值,更新后的结果value为value和delta之和,但是该方法会立即返回更新后的value值。
4、AtomicInteger的函数式接口

  自JDK1.8增加了函数式接口之后,AtomicInteger也提供了对函数式接口的支持。

  • int getAndUpdate(IntUnaryOperator updateFunction):原子性地更新AtomicInteger的值,方法入参为IntUnaryOperator接口,返回值为value更新之前的值。其中,IntUnaryOperator为函数式接口,有且仅有一个接口方法(非静态,非default),接口方法的返回值即AtomicInteger被更新后的value的最新值。
  • int updateAndGet(IntUnaryOperator updateFunction):类似getAndUpdate()方法,原子性地更新AtomicInteger的值,方法入参为IntUnaryOperator接口,该方法会立即返回更新后的value值。
  • int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction):原子性地更新AtomicInteger的值,方法入参为IntBinaryOperator接口和delta值x,返回值为value更新之前的值。IntBinaryOperator为函数式接口,有且仅有一个接口方法(非静态,非default),接口方法的返回值即AtomicInteger被更新后的value的最新值。
  • int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction):该方法与getAndAccumulate类似,只不过会立即返回AtomicInteger的更新值。
5、AtomicInteger的其他方法
  • int get():返回AtomicInteger的value当前值。
  • void set(int newValue):为AtomicInteger的value设置一个新值,因为在AtomicInteger中,value属性被volatile关键字修饰,即在写操作的前后都加了内存屏障,所以调用set方法为value设置新值后其他线程就会立即看见。
  • void lazySet(int newValue):不直接的操作value字段,而是通过Unsafe类的putOrderedInt方法先通过初始化时候计算出的vlaue字段的偏移变量找到字段地址,然后调用本地方法进行操作的,在本地方法中只在写操作前面加了一个屏障,而后面没有加。比起set()方法效率更高。

三、格物致知

  在前面了解了AtomicInteger原子类操作后,我们开始探究这些原子类操作是如何实现的,其中涉及的原理又是什么呢?在学习AtomicInteger实现逻辑中,首先要学习Unsafe 类的实现,其实AtomicInteger的实现主要就是依靠该类实现操作的原子性的。Unsafe 类的具体信息可以参考《Java并发——CAS Unsafe Atomic》这篇内容。

private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long valueOffset;

private volatile int value;

public AtomicInteger(int initialValue) {
	value = initialValue;
}
public AtomicInteger() {
}

  首先,AtomicInteger定义了两个构造函数,前面已经分析过他们的用法了。同时,还定义了Unsafe 对象和value、valueOffset两个属性字段,Unsafe 对象提供了硬件级别的原子操作,而value属性是一个被volatile关键字修饰的成员变量value,实际上,AtomicInteger所提供的所有方法主要都是针对该变量value进行的操作,valueOffset字段存储value属性据对象地址的偏移量。

static {
   try {
         valueOffset = unsafe.objectFieldOffset
             (AtomicInteger.class.getDeclaredField("value"));
     } catch (Exception ex) { throw new Error(ex); }
 }

  静态代码块实现valueOffset属性的初始化,借助unsafe的objectFieldOffset方法实现,该方法是一个本地方法,返回属性相对于对象的偏移量,这里使用反射获取属性。

public final int getAndIncrement() {
   return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int incrementAndGet() {
   return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
}
public final int decrementAndGet() {
   return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}

  AtomicInteger的自增自减方法,都是借助unsafe的getAndAddInt()方法实现原子操作的,具体实现如下:

//Unsafe类,获取内存地址为obj+offset的变量值, 并将该变量值加上delta
public final int getAndAddInt(Object obj, long offset, int delta) {
    int v;
    do {
    	//获取当前被volatile关键字修饰的value值(通过内存偏移量的方式读取内存)
        v= this.getIntVolatile(obj, offset);
    /*
	while中的compareAndSwapInt()方法尝试修改v的值,具体地, 该方法也会通过obj和offset获取变量的值
	如果这个值和v不一样, 说明其他线程修改了obj+offset地址处的值, 此时compareAndSwapInt()返回false, 继续循环
	如果这个值和v一样, 说明没有其他线程修改obj+offset地址处的值, 此时可以将obj+offset地址处的值改为v+delta, compareAndSwapInt()返回true, 退出循环
	Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改obj+offset地址处的值的时候不会被其他线程中断
	*/
    } while(!this.compareAndSwapInt(obj, offset, v, v + delta));

    return v;
}

  其中,compareAndSwapInt()方法,是一个native方法,提供了CAS(Compare And Swap)算法的实现,AtomicInteger类中的原子性方法几乎都借助于该方法实现。其中:

  • obj 该入参是地址偏移量所在的宿主对象
  • offset 该入参是obj宿主对象的地址偏移量,是由Unsafe对象获得。
  • v 该参数是我们期望的当前值,与当前实际值不相等,就会修改失败,方法也会返回false
  • v+delta,表示新值
CAS算法

  CAS 全称Compare-and-Swap,广义上的讲是CAS重入算法和CAS硬件操作。它采用乐观锁的方式,即认为不需要加锁,如果有多线程在同一时间段对同一个变量或者引用的指针进行操作,CAS的原理是,不加锁,本地变量value_0,先保存变量value_0的副本value_0_copy到栈空间,然后申明一个期望值value_update,利用硬件的CAS指令支持(如果不支持,则采用CAS重入算法,原理同上一句的操作),比较新老值value_0_copy和value_0,如果发现老值和新获得的值做比较,如果变化了,说明别的线程对这个值有改变,则认为更新失败,返回,至于返回失败之后是否需要重新执行,需要使用者进行判断,但是会存在ABA的问题,如果ABA问题不影响逻辑,对结果没有副作用,则不需要考虑,如果对结果有副作用,则需要考虑使用其它方式。即CAS算法包含3个操作数:内存值V、旧的预期值A、要修改的新值B。当且仅当预期值A与内存值V相等时,将内存值V修改为B,否则什么都不需要做。

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

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

public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

  上述方法,借助了unsafe对象的getAndAddInt()和compareAndSwapInt()方法实现了原子性操作,这两个方法前面也进行了分析,这里不再赘述。

public final int getAndUpdate(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();
        next = updateFunction.applyAsInt(prev);
    } while (!compareAndSet(prev, next));
    return prev;
}

public final int updateAndGet(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();
        next = updateFunction.applyAsInt(prev);
    } while (!compareAndSet(prev, next));
    return next;
}

public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) {
    int prev, next;
    do {
        prev = get();
        next = accumulatorFunction.applyAsInt(prev, x);
    } while (!compareAndSet(prev, next));
    return prev;
}

public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) {
    int prev, next;
    do {
        prev = get();
        next = accumulatorFunction.applyAsInt(prev, x);
    } while (!compareAndSet(prev, next));
    return next;
}

  上述几个方法都借助了compareAndSet()方法实现,其实底层还是借助了unsafe.compareAndSwapInt()方法实现,同时,这里使用了CAS算法,保证了操作的原子性操作。

public final int get() {
   return value;
}
public final void set(int newValue) {
    value = newValue;
}

public final void lazySet(int newValue) {
   unsafe.putOrderedInt(this, valueOffset, newValue);
}

  lazySet()方法实现了对value的非volatile赋值,通过调用unsafe.putOrderedInt()方法,直接向固定偏移量的内存上写入数据,但不使其对其他线程立刻可见(putOrderedInt()是putIntVolatile()的延迟实现)。
  lazySet()方法存在的意义是在某些不需要volatile的场景下,通过延迟赋值提高程序运行的效率。这个方法很少被上层调用者使用。

四、总结

  通过上述的学习,我们基本上了解了原子操作类AtomicInteger的用法和实现原理,其中需要关注的就是CAS算法和unsafe的本地方法借助硬件保证了原子操作,后续将继续学习其他原子操作类。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68710.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!