AtomicInteger、Unsafe类、ABA问题

AtomicInteger

Java中的AtomicInteger大家应该很熟悉,它是为了解决多线程访问Integer变量导致结果不正确所设计的一个基于多线程并且支持原子操作的Integer类。

AtomicInteger内部有一个变量UnSafe:private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe类是一个可以执行不安全、容易犯错的操作的一个特殊类。虽然Unsafe类中所有方法都是public的,但是这个类只能在一些被信任的代码中使用。Unsafe的源码可以在这里看 -> UnSafe源码

Unsafe类可以执行以下几种操作:

  1. 分配内存,释放内存:在方法allocateMemory,reallocateMemory,freeMemory中,有点类似c中的malloc,free方法
  2. 可以定位对象的属性在内存中的位置,可以修改对象的属性值。使用objectFieldOffset方法
  3. 挂起和恢复线程,被封装在LockSupport类中供使用
  4. CAS操作(CompareAndSwap,比较并交换,是一个原子操作)

AtomicInteger中用的就是Unsafe的CAS操作。

Unsafe中的int类型的CAS操作方法:

public final native boolean compareAndSwapInt(Object o, long offset,
                                                int expected,
                                                int x);

参数o就是要进行cas操作的对象,offset参数是内存位置,expected参数就是期望的值(旧值),x参数是需要更新成的新值。

也就是说,如果我把valueOffset地址的值更新为2,valueOffset原来的值是1,需要这样调用:

compareAndSwapInt(this, valueOffset, 1, 2)

执行后valueOffset地址的变为2或者失败。分析在后面。

valueOffset字段表示内存位置,可以在AtomicInteger对象中使用unsafe得到:

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

AtomicInteger内部使用变量value表示当前的整型值,这个整型变量还是volatile的,表示内存可见性,一个线程修改value之后保证对其他线程的可见性:

private volatile int value;

AtomicInteger内部还封装了一下CAS,定义了一个compareAndSet方法,只需要2个参数:

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

不论是addAndGet、incrementAndGet、getAndAdd都是在调用unsafe的getAndAddInt方法,然后对返回值进行+/-delta操作。请注意返回的是未修改之前的值,而不是修改后的值。

getAndAddInt 参数: var1 = this, var2 = valueOffset, var4 = delta

compareAndSwapInt 参数:var1 = this, var2 = valueOffset, var5 = 旧值, var5 + var4 = 要修改成的值

 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
}

以addAndGet方法为例:

getAndAdd调用方法:unsafe.getAndAddInt(this, valueOffset, delta);方法内部使用一个do_while循环,先得到当前的值var5,然后再把当前的值加var4,加完之后使用cas原子操作让当前值加var4处理正确。当然cas原子操作不一定是成功的,所以做了一个死循环,当cas操作成功的时候返回数据。这里由于使用了cas原子操作,所以不会出现多线程处理错误的问题。比如线程A得到var5为1,线程B也得到var5为1;线程A的var5 + var4值为2,进行cas操作并且成功的时候,将var5修改成了2;这个时候线程B得到var5 + var4值仍旧为2,当进行cas操作的时候由于内存中的值已经是2,而不是1了;所以cas操作会失败,下一次循环的时候得到的var5就变成了2;也就不会出现多线程处理问题了。

虽然AtomicInteger中的cas操作可以实现非阻塞的原子操作,但是会产生ABA问题。

ABA问题

CAS看起来很爽,但是会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。这允许一对变化的元素进行原子操作。

在运用CAS做Lock-Free操作中有一个经典的ABA问题:

线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题,例如下面的例子:

A

现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

B

此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

C

其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败。

非阻塞同步算法与CAS(Compare and Swap)无锁算法

Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都采用独占的方式来访问这些变量,如果出现多个线程同时访问锁,那第一些线线程将被挂起,当线程恢复执行时,必须等待其它线程执行完他们的时间片以后才能被调度执行,在挂起和恢复执行过程中存在着很大的开销。锁还存在着其它一些缺点,当一个线程正在等待锁时,它不能做任何事。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。如果被阻塞的线程优先级高,而持有锁的线程优先级低,将会导致优先级反转(Priority Inversion)。

独占锁:是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。

乐观锁:每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。

与锁相比,volatile变量是一和更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile变量也存在一些局限:不能用于构建原子的复合操作,因此当一个变量依赖旧值时就不能使用volatile变量。

非阻塞算法 (nonblocking algorithms)

一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

CAS 操作

乐观锁用到的机制就是CAS,Compare and Swap。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。


参考资料:
网络资料
Java8源码
备注:
转载请注明出处:http://blog.csdn.net/wsyw126/article/details/53979918
作者:WSYW126

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值