java基础总结(五十四)--AtomicInteger使用时注意事项

目录

 

1来自AtomicInteger类真的是线程安全的嘛?

2关于AtomicInteger原理方面的讲解

AtomicInteger

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

非阻塞算法 (nonblocking algorithms)

CAS 操作


1来自AtomicInteger类真的是线程安全的嘛?

public final int incrementAndGet() {
        for (;;) {
            int current = get();//1
            int next = current + 1;//2
            if (compareAndSet(current, next))//3
                return next;
        }
    }
以上是源代码,这个操作相当于i++

如果有两个AtomicInteger,i1和i2,i1执行到步骤2,自己栈中的值变成5,没有执行步骤3,因为步骤3才是是写操作,所以此时i1自己的栈中的值是加1后的5,但是主内存中的值还是4,此时i2通过get()得到当前主内存的值,4,进行加1操作,变成5.

到此,i1和i2在他们各自的栈中都是5,现在他们把5这个值写回主内存,i1成功了,它执行的是compareAndSet(4, 5);
i2在执行步骤3的时候,执行的也是compareAndSet(4, 5);但是,此时主内存中第一个参数对应的值已经被i1改成5,所以此次操作失败,i2进入第二个循环,通过步骤1获得当前值5,然后加1,所以最后i2把值变成了6.

如果i2的线程中代码是这样的:
if (i2 == 4)
{
    i2.incrementAndGet();//预期是5,但是实际上是6
}

 

2关于AtomicInteger原理方面的讲解

只看了前半截觉得写很好后半部分没看哦。来自https://blog.csdn.net/WSYW126/article/details/53979918

AtomicInteger


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

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

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

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

分配内存,释放内存:在方法allocateMemory,reallocateMemory,freeMemory中,有点类似c中的malloc,free方法
可以定位对象的属性在内存中的位置,可以修改对象的属性值。使用objectFieldOffset方法
挂起和恢复线程,被封装在LockSupport类中供使用
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,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

head.compareAndSet(A,B);

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

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

其中堆栈中只有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,否则什么都不做。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章