Java并发:CAS、ABA问题、ABA问题解决方案

【1】锁

1、加锁的机制

参见:java线程安全和锁机制详解

网址:http://smallbug-vip.iteye.com/blog/2275743

2、锁的机制有如下问题

(1)在多线程环境下,加锁、释放锁会导致比较多的上下文切换和调度延时,从而引起性能问题。

(2)一个线程持有锁会导致其他所有需要此锁的线程挂起。

(3)如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

3、悲观锁和乐观锁

(1)独占锁:是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。 乐观锁:每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。

(2)volatile是不错的机制,但是volatile不能保证原子性。因此,对于同步问题最终还是要回到锁机制上来。

【优质文章】深入理解Java内存模型(四)——volatile:http://www.open-open.com/lib/view/open1459412319988.html 

【2】CAS方法:CompareAndSwap

1、乐观锁的使用的机制就是CAS。

在CAS方法中,CAS有三个操作数,内存值V,旧的预期值E,要修改的新值U。当且仅当预期值E和内存值V相等时,将内存值V修改为U,否则什么都不做。

2、非阻塞算法(nonblocking algorithms):一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

(1)非阻塞算法简介:https://www.ibm.com/developerworks/cn/java/j-jtp04186/

(2)非阻塞算法通常叫作乐观算法,因为它们继续操作的假设是不会有干扰。如果发现干扰,就会回退并重试。

3、CAS方法

(1)CompareAndSwap()就使用了非阻塞算法来代替锁定。

(2)举例:AtomicInteger来研究在没有锁的情况下是如何做到数据正确性的。

在没有锁机制的情况下,要保证线程间的数据是可见的,就会常常用到volatile原语了。

private volatile int value;

可使用如下方法读取内存变量值value: 

public final int getValue(){
    return value;
}

递增计数器是如何实现的:

public final int incrementAndGet(){
    for(;;){
        int current = getValue();
        int next = value + 1;
        if(compareAndSet(current,next)){
            return next;
        }
    }
}

该方法采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。

而compareAndSet操作利用了Java本地接口(JNI,Java Native Interface)完成CPU指令的操作:

public final boolean compareAndSet(int expect, int update){
    return unsafe.compareAndSwapInt(this,valueOffset,expect,unsafe);
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
      try {
        valueOffset = unsafe.objectFieldOffset(*.class.getDeclaredField("value"));
      } catch (Exception ex) { 
      throw new Error(ex); }
}

【3】“ABA”问题

1、可以发现,CAS实现的过程是先取出内存中某时刻的数据,在下一时刻比较并替换,那么在这个时间差会导致数据的变化,此时就会导致出现“ABA”问题。

2、什么是”ABA”问题?

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。 尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

ABA问题是一种异常现象:如果在算法中的节点可以被循环使用,那么在使用“比较并交换”指令时就可能出现这个问题(如果在没有垃圾回收机制的环境 中)。在CAS操作中将判断“V的值是否仍然为A?”,并且如果是的话就继续执行更新操作。在大多数情况下,这种判断是足够的。然而,有时候还需要知道 “自从上次看到V的值为A以来,这个值是否发生了变化?”在某些算法中,如果V值首先由A编程B,在由B编程A,那么仍然被认为发生了变化,并需要重新执 行算法中的某些步骤。  

如果在算法中采用自己的方式来管理节点对象的内存,那么可能出现ABA问题。在这种情况下,即使链表的头结点仍然只想之前观察到的节点,那么也不足 以说明链表的内容没有发生变化。如果通过垃圾回收器来管理链表节点仍然无法避免ABA问题,那么还有一个相对简单的解决方法:不是只是更新某个引用的值, 而是更新两个值,包含一个引用和一个版本号。即使这个值由A变成B,然后又变为A,版本号也将是不同的。AtomicStampedReference以 及AtomicMarkableReference支持在两个变量上执行原子的条件更新。AtomicStampedReference将更新一个“对象 —-引用”二元组,通过在引用上加上“版本号”,从而避免ABA问题。类似地,AtomicMarkableReference将更新一个“对象引用—- 布尔值”二元组,在某些算法中将通过这种二元组使节点保存在链表中同时又将其标记为“已删除节点”。 

【4】用AtomicStampedReference/AtomicMarkableReference解决ABA问题

1、原子操作:http://www.blogjava.net/xylz/archive/2010/07/02/325079.html

2、用AtomicStampedReference解决ABA问题:http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

3、高并发Java(4):无锁: http://www.importnew.com/21282.html

4、AtomicStampedReference、AtomicMarkableReference源码分析,解决cas ABA问题: https://blog.csdn.net/zqz_zqz/article/details/68062568

5、关于AtomicStampedReference使用的坑: https://blog.csdn.net/xybz1993/article/details/79992120

6、JAVA中的CAS: https://blog.csdn.net/mmoren/article/details/79185862

7、看看别人的Java面试 你是否又有学习的动力了?: http://www.jizhuomi.com/software/707.html 
 

 

你可能感兴趣的:(Java学习)