JUC之Atomic原子类型

一、线程安全问题


并发编程(多线程编程)首先要解决的问题就是线程安全问题,我们常用的一些Java基本类操作在单线程执行过程中是没有问题,但是一旦到多线程环境中就会出现失常,比如:

int++存在线程安全问题,即多线程执行int++后结果会与预期有出入,不具备原子性。

public class AtomicIntegerTest {
    static int num=0;
    static AtomicInteger ai = new AtomicInteger(0);
    
    public static void main(String[] args) throws Exception {
    	//计数器
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        ExecutorService executorService = Executors.newFixedThreadPool(200);
    
        for(int i=0; i < 1000; i++){
            executorService.execute(()->{
                num++;
                ai.getAndIncrement();
                countDownLatch.countDown();
            });
        }
    	//阻塞,等待所有线程都执行完成
        countDownLatch.await();
        executorService.shutdown();
    
        System.out.println("AtomicInteger result:"+ai.get());
        System.out.println("Integer result:"+num);
    }
}

//输出结果
AtomicInteger result:1000   (始终输出1000, 线程安全)
Integer result:998int输出的结果偶尔是1000,说明非线程安全)

以上例子说明全局变量int会存在线程安全问题,可以使用synchronized、Lock或AtomicInteger来解决这个问题,通过基准测试(JMH)性能结果AtomicInteger>显式锁Lock>synchronized关键字。

二、AtomicInteger源码分析


以下是AtomicInteger的大致结构,核心就是借助Unsafe的各种CAS native方法实现线程安全。

// Unsafe是由C++实现的,其内部存在着大量的汇编 CPU指令等代码,JDK实现的
// Lock Free几乎完全依赖于该类
private static final Unsafe unsafe = Unsafe.getUnsafe();
// valueOffset将用于存放value的内存地址偏移量
private static final long valueOffset;
static {
    try {
        // 获取value的内存地址偏移量
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
// 在AtomicInteger的内部有一个volatile修饰的int类型成员属性value 
private volatile int value;

// AtomicInteger中的原子性方法都借助于unsafe.compareAndSwapInt方法
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

1. compareAndSwapInt源码分析——CAS算法

/*
* 该方法是一个native方法,需查看jdk源码(C++实现)
* 打开openjdk-jdk8u/hotspot/src/share/vm/prims/unsafe.cpp文件我们会找到相关的C++代码
* object:该入参是地址偏移量所在的宿主对象
* valueOffSet:该入参是object对象某属性的地址偏移量,是由Unsafe对象获得的
* expectValue:该值是我们期望value当前的值,如果expectValue与实际的当前
*              值不相等,那么对value的修改将会失败,方法的返回值也会变为false
* newValue:新值
*/
public final native boolean compareAndSwapInt(Object object, long valueOffSet,  int expectValue, int newValue);

CAS包含3个操作数:内存值V、旧的预期值A、要修改的新值B。当且仅当预期值A与内存值V相等时,将内存值V修改为B,否则什么都不需要做,这种方式也被称为乐观锁。

2. 自旋方法addAndGet源码分析

由于compareAndSwapInt方法的乐观锁特性,会存在对value修改失败的情况,但是有些时候对value的更新必须要成功,比如调用incrementAndGet、addAndGet等方法,分析一下addAndGet方法的实现:

// AtomicInteger类中的addAndGet方法
/**
 * Atomically adds the given value to the current value.
 *
 * @param delta the value to add
 * @return the updated value
 */
public final int addAndGet(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}


// Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object object, long valueOffset, int delta) {
    int currentValue;
    do {
        //获取当前被volatile关键字修饰的value值(通过内存偏移量的方式读取内存)
        currentValue= this.getIntVolatile(object, valueOffset);
        //执行compareAndSwapInt方法,如果执行成功则直接返回,如果执行失败则再次执行下一轮的compareAndSwapInt方法
    } while(!this.compareAndSwapInt(object, valueOffset, currentValue, currentValue+ delta));
    return currentValue;
}

可以看到本质还是调用compareAndSwapInt方法,通过循环比对直到成功,形成自旋。

通过上面源码的分析,以下代码incrementAndGet的执行结果有可能是11也有可能是比11更大的值

AtomicInteger ai = new AtomicInteger(10);
//这句断言在多线程的情况下未必会成功
assert ai.incrementAndGet() == 11;

AtomicLong、AtomicBoolean类似。

三、AtomicReference详解


上述的AtomicInteger、AtomicLong、AtomicBoolean都是针对封装类型的原子类型,在实际业务场景中不能完全满足,比如个人的资金账户变化,就需要针对自定义对象Object的原子类型,即AtomicReference。

public class User {

    private String account;
    private int amount;

    public User(String account, int amount) {
        this.account = account;
        this.amount = amount;
    }
    
    //... getter and setter
}
public class AtomicReferenceTest {
    static ExecutorService  executorService = Executors.newFixedThreadPool(50);
    final static int loopCount = 100;

    // 使用volatile线程共享
    private static volatile User user = new User("Kevin", 0);
    // 金额非同步增加10
    public static void inc(){
        user.setAmount(user.getAmount()+10);
    }

    private static volatile User syncUser=new User("Kevin", 0);
    // 金额同步增加10
    public static void syncInc(){
        synchronized (AtomicReferenceTest.class){
            final User u = syncUser;
            final User newUser = new User(u.getAccount(), u.getAmount()+10);
            syncUser = newUser;
        }
    }

    // 使用AtomicReference具有原子性,此处使用的compareAndSet,可能会失败,所以循环处理
    private static AtomicReference<User> ref=new AtomicReference<>(new User("Kevin", 0));
    public static void casInc(){
        boolean isOk = false;
        while (!isOk){
            final User u = ref.get();
            final User newUser = new User(u.getAccount(), u.getAmount()+10);
            isOk=ref.compareAndSet(u, newUser);
            if(!isOk){
                System.out.println(Thread.currentThread().getName()+",continue:"+newUser);
            }
        }
    }

    public static void testAll() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(loopCount);

        for(int i=0; i < loopCount; i++){
            executorService.execute(()->{
                inc();
                syncInc();
                casInc();
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();

        System.out.println("Integer result:"+user.getAmount());
        System.out.println("Sync result:"+syncUser.getAmount());
        System.out.println("Atomic result:"+ref.get().getAmount());
    }
     public static void main(String[] args) throws InterruptedException {
        testAll();
        executorService.shutdown();
    }
}


// 运行结果
pool-1-thread-1,continue:User{account='Kevin', amount=10}   (偶尔会出现,说明cas失败了,通过循环自旋处理)
Integer result:990    (说明非线程安全)
Sync result:1000
Atomic result:1000

以上通过三种方式进行测试,普通方式进行多线程加减存在线程安全问题,通过synchronized同步锁和AtomicReference都可以实现线程安全操作。

四、AtomicStampedReference详解(解决ABA问题)


原子类型用自旋+CAS的无锁操作保证了共享变量的线程安全性和原子性,绝大多数情况下,CAS算法并没有什么问题,但是在需要关心变化值的操作中会存在ABA的问题,比如一个值原来是A,变成了B,后来又变成了A,那么CAS检查时会发现它的值没有发生变化,但是实际上却是发生了变化的。

比如账户有10元,给小于20元的账户赠送100元,只能一次:
A线程增加100元账户变成了110,B线程刚好消费减少100元账户又变成了10元,C线程无法知道账户是否有过变化又会重复赠送

如何避免CAS算法带来的ABA问题呢?针对乐观锁在并发情况下的操作,我们通常会增加版本号,在Java原子包中也提供了这样的实现AtomicStampedReference。
AtomicStampedReference创建时不仅需要指定初始值,还需要设定stamped的初始值,在AtomicStampedReference的内部会将这两个变量封装成Pair对象,如下:

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;

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

使用AtomicStampedReference进行测试

// 使用AtomicStampedReference具有原子性
private static AtomicStampedReference<User> stampRef=new AtomicStampedReference<>(new User("Kevin", 0), 1);
public static void casStampInc(){
    boolean isOk = false;
    while (!isOk){
        final User u = stampRef.getReference();
        final User newUser = new User(u.getAccount(), u.getAmount()+10);
        final int stamp = stampRef.getStamp();
        isOk=stampRef.compareAndSet(u, newUser, stamp, stamp+1);
        if(!isOk){
			System.out.println(Thread.currentThread().getName()+",stamp continue:"+newUser);
        }
    }
}

public static void testAll() throws InterruptedException {
    System.out.println("Atomic Stamp result:"+stampRef.getReference().getAmount()+","+stampRef.getStamp());
}

//运行结果
pool-1-thread-32,stamp continue:User{account='Kevin', amount=330}
Atomic Stamp result:1000,101

如上stamp可以用来标记值的变化版本。

五、AtomicArray数组原子类型


针对数组的原子类型如下:
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

// 定义int类型的数组并且初始化
int[] intArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 创建AtomicIntegerArray 并且传入int类型的数组
AtomicIntegerArray intAtomicArr = new AtomicIntegerArray(intArray);

// 原子性地为intAtomicArr的第二个元素加10
assert intAtomicArr.addAndGet(1, 10) == 12;

// 第二个元素更新后值为12
assert intAtomicArr.get(1) == 12;

六、AtomicFieldUpdater原子性更新


既不想使用synchronized对共享数据的操作进行同步,又不想将数据类型声明成原子类型的,可以使用AtomicFieldUpdater原子性操作对象属性,如AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

// 定义一个简单的类
static class Alex{
    // int类型的salary,并不具备原子性的操作
    volatile int salary;
    public int getSalary()
    {
        return this.salary;
    }
}

public static void main(String[] args){
    // 定义AtomicIntegerFieldUpdater,通过newUpdater方法创建
    AtomicIntegerFieldUpdater<Alex> updater = AtomicIntegerFieldUpdater.newUpdater(Alex.class, "salary");
    // 实例化Alex
    Alex alex = new Alex();
    // 原子性操作Alex类中的salary属性
    int result = updater.addAndGet(alex, 1);
    assert result == 1;
}

AtomicFieldUpdater在使用上非常简单,但是并不是所有的成员属性都适合被原子性地更新:
(1)未被volatile关键字修饰的成员属性无法被原子性地更新
(2)类变量(static修饰)无法被原子性地更新
(3)无法直接访问的成员属性不支持原子性地更新
(4)final修饰的成员属性无法被原子性地更新
(5)父类的成员属性无法被原子性地更新

七、总结


原子类型包为我们提供了一种无锁的原子性操作共享数据的方式,无锁的操作方式可以减少线程的阻塞,减少CPU上下文的切换,提高程序的运行效率,但是这并不是一条放之四海皆准的规律,比如,同样被synchronized关键字同步的共享数据和原子类型的数据在单线程运行的情况下,synchronized关键字的效率却要高很多,究其原因是synchronized关键字是由JVM提供的相关指令所保证的,因此在Java程序运行期优化时可以将同步擦除,而原子类是由本地方法和汇编指令来提供保障的,在Java程序运行期间是没有办法被优化的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值