从concurrent下的Atomic原子类说起

9 篇文章 0 订阅

前言

在使用多线程并发的时候,我们经常会使用到JDK1.5版本开始引入的原子处理类,如AtomicInteger、AtomicBoolean、AtomicReference、AtomicStampedReference等。
本文将从AtomicInteger的实现出发,探讨多线程下的无锁实现及相关的知识,包括乐观锁与悲观锁及CAS算法思想。


AtomicInteger介绍

AtomicInteger是什么

AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减或设值。

AtomicInteger的使用场景

AtomicInteger提供原子操作来进行Integer的操作,因此十分适合高并发情况下使用。

AtomicInteger的主要API

public final int get(); // 取得当前值
public final void set(int newValue);    // 设置当前值
public final int getAndSet(int newValue);   // 设置新值,并返回旧值
public final boolean compareAndSet(int expect, int u);  // 如果当前值为expect,则设置为u
public final int getAndIncrement(); // 当前值加1,返回旧值,相当于i++
public final int incrementAndGet(); // 当前值加1,返回新值,相当于++i
public final int addAndGet(int delta);  // 当前值增加delta,返回新值
public final int getAndDecrement(); // 当前值减1,返回旧值,相当于i--
public final int decrementAndGet(); // 当前值减1,返回新值,相当于--i
public final int getAndAdd(int delta);  // 当前值增加delta,返回旧值

AtomicInteger的源码解析

以AtomicInteger.getAndIncrement()为例,在JDK1.8上的源码实现如下:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

可以看出AtomicInteger直接调用了Unsafe中的getAndAddInt()方法。

Unsafe类简介

Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。
UnSafe类提供了很多底层的操作,包括内存管理、非常规的对象实例化、数组操作、多线程同步(包括锁机制、CAS操作)、挂起与恢复等操作。
继续进入Unsafe类中查看

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
public native int getIntVolatile(Object o, long offset);
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

这里可以看出,实现线程安全的getAndIncrement()是通过while循环里不停地调用native的getIntVolatile()方法,直到compareAndSwapInt()方法返回true为止。
为什么设置一个值需要在一个循环里面不停地执行?原因就是这里应用了并发设计中的CAS算法。

悲观锁、乐观锁及CAS的思想

我们平时用的锁(synchronized,Lock)是一种悲观的策略。它总是假设每一次临界区操作会产生冲突。因此,必须对每次操作都小心翼翼。如果多个线程同时访问临界区资源,就宁可牺牲性能让线程进行等待,所以锁会阻塞线程执行。这里称之为悲观锁。

与之相对的有一种乐观的策略,我们可以称之为无锁,也即乐观锁
在无锁的调用中,一个典型的特点是可能包含一个无穷循环。在这个循环中,线程会不断尝试修改共享变量。如果没有冲突,修改成功,那么退出循环,否则继续尝试修改。但无论如何,无锁的并行总能保证有一个线程是可以胜出,不至于全军覆没。至于临界区中竞争失败的线程,它们则必须不断重试,直到自己获胜。如果运气很不好,总是尝试不成功,就会出现类似饥饿的现象,线程停滞不前。

CAS(Compare and swap)即比较交换,是设计并发算法时用到的一种技术,可以用它来实现无锁。简单来说,CAS使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
由于CAS算法是非阻塞的,它对死锁问题天生免疫,而且它比基于锁的方式拥有更优越的性能

CAS的实现

CAS算法的过程是这样:它包含三个参数 CAS(V,E,N)。V表示要更新的变量,E表示预期的值,N表示新值。仅当V值等于E值时,才会将V的值设置成N,否则什么都不做。最后CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试。
在硬件层面,大部分的现代处理器都已经支持原子化的CAS指令。

CAS的缺点-ABA问题

线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望值是否一致。这个逻辑从一般意义上来说是正确的。但是可能有一个小小的例外:当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了两次,而经过这两次修改后,对象的值又恢复为旧值。这样,当前线程就无法判断这个对象究竟是否被修改过。此时有可能会导致意想不到的结果。这就是ABA问题
比如两个线程
线程1 查询A的值为a,与旧值a比较,
线程2 查询A的值为a,与旧值a比较,相等,更新为b值
线程2 查询A的值为b,与旧值b比较,相等,更新为a值
线程1 相等,更新B的值为c
可以看到这样的情况下,线程1 可以正常 进行CAS操作,将值从a变为c 但是在这之间,实际A值已经发了a->b b->a的转换。
为此可引入版本号或时间戳来解决这个问题。例如在JDK中的AtomicStampedReference类,它的内部不仅维护了对象值,还维护了一个时间戳。当AtomicStampedReference的值被修改时,除了更新数据本身外,还必须更新时间戳。当设置值时,只有对象值及时间戳都满足期望值时,写入才成功。

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);
}

后记

JDK中的Atomic原子类使用了CAS实现多线程并发操作。相比使用互斥锁,CAS实现的是自旋锁,有着较好的性能,但也有它的局限性及适用场景。
对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗CPU资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,自旋的几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
关于自旋锁与互斥锁的对比和优缺点,仍有待后续学习研究。本次由Atomic原子类为楔子所带起的相关知识就介绍到此,CAS更底层的实现可参考博客的另外一篇文章:https://mp.csdn.net/mdeditor/79415104


Written By 欧晓星

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值