高并发面试必问:CAS 引起ABA问题解决方案
Posted 程序员石磊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高并发面试必问:CAS 引起ABA问题解决方案相关的知识,希望对你有一定的参考价值。
该文章来自《java高并发核心编程》,说来惭愧在阅读该书之前只知道用版本号解决,不知道Jdk已经提供了实现,下面来揭开神秘面试吧!
很多乐观锁的实现版本都是使用版本号(Version)方式来解决ABA问题。乐观锁每次在执行数据的修改操作时都会带上一个版本号,版本号和数据的版本号一致就可以执行修改操作并对版本号执行加1操作,否则执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加,不会减少。
使用AtomicStampedReference解决ABA问题
参考乐观锁的版本号,JDK提供了一个AtomicStampedReference类来解决ABA问题。AtomicStampReference在CAS的基础上增加了一个Stamp(印戳或标记),使用这个印戳可以用来觉察数据是否发生变化,给数据带上了一种实效性的检验。
AtomicStampReference的compareAndSet()方法首先检查当前的对象引用值是否等于预期引用,并且当前印戳(Stamp)标志是否等于预期标志,如果全部相等,就以原子方式将引用值和印戳(Stamp)标志的值更新为给定的更新值。AtomicStampReference的构造器有两个参数,具体如下:
//构造器,V表示要引用的原始数据,initialStamp表示最初的版本印戳(版本号)
AtomicStampedReference(V initialRef, int initialStamp)
AtomicStampReference常用的几个方法如下:
//获取被封装的数据
public V getRerference();
//获取被封装的数据的版本印戳
public int getStamp();
AtomicStampedReference的CAS操作的定义如下:
public boolean compareAndSet(
V expectedReference, //预期引用值
V newReference, //更新后的引用值
int expectedStamp, //预期印戳(Stamp)标志值
int newStamp) //更新后的印戳(Stamp)标志值
compareAndSet()方法的第一个参数是原来的CAS中的参数,第二个参数是替换后的新参数,第三个参数是原来CAS数据旧的版本号,第四个参数表示替换后的新参数版本号。
进行CAS操作时,若当前引用值等于预期引用值,并且当前印戳值等于预期印戳值,则以原子方式将引用值和印戳值更新为给定的更新值。
下面是一个简单的AtomicStampedReference使用示例,通过两个线程分别带上印戳更新同一个atomicStampedRef实例的值,第一个线程会更新成功,而第二个线程会更新失败,具体代码如下:
package com.crazymakercircle.cas;
// 省略import
public class AtomicTest
{
@Test
public void testAtomicStampedReference()
{
CountDownLatch latch = new CountDownLatch(2);
AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(1, 0);
ThreadUtil.getMixedTargetThreadPool().submit(new Runnable()
{
@Override
public void run()
{
boolean success = false;
int stamp = atomicStampedRef.getStamp();
Print.tco("before sleep 500: value="
+ atomicStampedRef.getReference()
+ " stamp=" + atomicStampedRef.getStamp());
//等待500毫秒
sleepMilliSeconds(500);
success = atomicStampedRef.compareAndSet(1, 10,
stamp, stamp + 1);
Print.tco("after sleep 500 cas 1: success=" + success
+ " value=" + atomicStampedRef.getReference()
+ " stamp=" + atomicStampedRef.getStamp());
//增加印戳值,然后更新,如果stamp被其他线程改了,就会更新失败
stamp++;
success = atomicStampedRef.compareAndSet(10, 1,
stamp, stamp+1);
Print.tco("after sleep 500 cas 2: success=" + success
+ " value=" + atomicStampedRef.getReference()
+ " stamp=" + atomicStampedRef.getStamp());
latch.countDown();
}
});
ThreadUtil.getMixedTargetThreadPool().submit(new Runnable()
{
@Override
public void run()
{
boolean success = false;
int stamp = atomicStampedRef.getStamp();
// stamp = 0
Print.tco("before sleep 1000: value="
+ atomicStampedRef.getReference()
+ " stamp=" + atomicStampedRef.getStamp());
//等待1000毫秒
sleepMilliSeconds(1000);
Print.tco("after sleep 1000: stamp = "
+ atomicStampedRef.getStamp());
//stamp = 1,这个值实际已经被修改了
success = atomicStampedRef.compareAndSet(
1, 20, stamp, stamp++);
Print.tco("after cas 3 1000: success=" + success
+ " value=" + atomicStampedRef.getReference()
+ " stamp=" + atomicStampedRef.getStamp());
latch.countDown();
}
});
latch.await();
}
// 省略其他
}
运行以上示例,输出结果如下:
[apppool-1-mixed-2]:before sleep 1000: value=1 stamp=0
[apppool-1-mixed-1]:before sleep 500: value=1 stamp=0
[apppool-1-mixed-1]:after sleep 500 cas 1: success=true value=10 stamp=1
[apppool-1-mixed-1]:after sleep 500 cas 2: success=true value=1 stamp=2
[apppool-1-mixed-2]:after sleep 1000: stamp = 2
[apppool-1-mixed-2]:after cas 3 1000: success=false value=1 stamp=2
使用AtomicMarkableReference解决ABA问题
AtomicMarkableReference是AtomicStampedReference的简化版,不关心修改过几次,只关心是否修改过。
因此,其标记属性mark是boolean类型,而不是数字类型,标记属性mark仅记录值是否修改过。AtomicMarkableReference适用于只要知道对象是否被修改过,而不适用于对象被反复修改的场景。
下面是一个简单的AtomicMarkableReference使用示例,通过两个线程分别更新同一个atomicRef的值,第一个线程会更新成功,而第二个线程会更新失败,具体代码如下:
package com.crazymakercircle.cas;
// 省略import
public class AtomicTest
{
@Test
public void testAtomicMarkableReference() throws InterruptedException
{
CountDownLatch latch = new CountDownLatch(2);
AtomicMarkableReference<Integer> atomicRef =
new AtomicMarkableReference<Integer>(1, false);
ThreadUtil.getMixedTargetThreadPool().submit(new Runnable()
{
@Override
public void run()
{
boolean success = false;
int value = atomicRef.getReference();
boolean mark = getMark(atomicRef);
Print.tco("before sleep 500: value=" + value
+ " mark=" + mark);
//等待500毫秒
sleepMilliSeconds(500);
success = atomicRef.compareAndSet(1, 10, mark, !mark);
Print.tco("after sleep 500 cas 1: success=" + success
+ " value=" + atomicRef.getReference()
+ " mark=" + getMark(atomicRef));
latch.countDown();
}
});
ThreadUtil.getMixedTargetThreadPool().submit(new Runnable()
{
@Override
public void run()
{
boolean success = false;
int value = atomicRef.getReference();
boolean mark = getMark(atomicRef);
Print.tco("before sleep 1000: value="
+ atomicRef.getReference()
+ " mark=" + mark);
//等待1000毫秒
sleepMilliSeconds(1000);
Print.tco("after sleep 1000: mark = " + getMark(atomicRef));
success = atomicRef.compareAndSet(1, 20, mark,!mark);
Print.tco("after cas 3 1000: success=" + success
+ " value=" + atomicRef.getReference()
+ " mark=" + getMark(atomicRef));
latch.countDown();
}
});
latch.await();
}
//取得修改标志值
private boolean getMark(AtomicMarkableReference<Integer> atomicRef)
{
boolean[] markHolder = {false};
int value = atomicRef.get(markHolder);
return markHolder[0];
}
// 省略其他
}
运行以上示例,输出结果如下:
[apppool-1-mixed-1]:before sleep 500: value=1 mark=false
[apppool-1-mixed-2]:before sleep 1000: value=1 mark=false
[apppool-1-mixed-1]:after sleep 500 cas 1: success=true value=10 mark=true
[apppool-1-mixed-2]:after sleep 1000: mark = true
[apppool-1-mixed-2]:after cas 3 1000: success=false value=10 mark=true
以上是关于高并发面试必问:CAS 引起ABA问题解决方案的主要内容,如果未能解决你的问题,请参考以下文章