多线程模型:

每个线程都有自己的独立内存空间,当线程需要操作主内存中的数据,需要先拷贝一份数据到自己的内存空间,然后进行修改,再刷回主内存
CAS:
CompareAndSwap,比较然后替换,多线程解决数据错乱的方案,如上图,试想,i=10表示10本书,t1,t2线程同时进行销售书,如果某一时间,t1和t2线程同时将i=10拷贝到自己的数据空间,t1将书减1,i=9再刷回主内存,即现在书的数目已经为9了,然后t2线程将的工作内存还为10它也进行减1然后刷回主内存,这样主内存的i值还是为9,这样就导致了已经卖了两本书,但是书的数目却只减少了1,CAS便是在t2线程进行减了操作后,需要刷回主内存的时候,将取到的值与主内存的值作比较,相同再进行设置,不同则设置失败。
ABA:
有了CAS可以保证操作数目一致,但是也出现一个问题,例如,t1线程先将书的数目减了1然后别人退还一本书,书的数目又加了1,也就是数的数目经历了10->9->10,然后t2线程操作时,发现主内存还是为10,所以觉得没问题就进行操作了,可能这个例子不能很好的描述问题,但是在某些案例中,会给人一种狸猫换太子的感觉,贴一个图

图片来自:https://baijiahao.baidu.com/s?id=1648077822185803003&wfr=spider&for=pc
一个小偷,把别人家的钱偷了之后又还了回来,还是原来的钱吗,你老婆出轨之后又回来,还是原来的老婆吗?ABA问题也一样,如果不好好解决就会带来大量的问题。最常见的就是资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律。
解决方案:
AtomicStampedReference
主要思想:通过加版本控制来进行修改,代码如下:
public class AtomicStampedReferenceTest {
// 初始化,值为100,版本为1
static AtomicStampedReference<Integer> integerReference =
new AtomicStampedReference<>(100,1);
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
int stamp = integerReference.getStamp();
System.out.println("t1第一次版本:"+stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(100, 101,
stamp, stamp + 1));
stamp = integerReference.getStamp();
System.out.println("t1第二次版本:"+stamp);
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(101,100,
stamp,stamp+1));
},"t1");
Thread t2 = new Thread(() -> {
int stamp = integerReference.getStamp();
System.out.println("t2第一次版本:"+stamp);
// 睡眠3秒保证t1完成一次ABA操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2修改成功?:"+integerReference.compareAndSet(100,500,
stamp,stamp+1));
System.out.println("t2:期望版本是:"+stamp+"--当前版本是:"+integerReference.getStamp());
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
- 代码解析:
- 首先t1,t2线程同时拿到对象的最初版本1,值为100
2.t2 线程等待3秒,保证t1线程将值修改为101,然后再将值修改为100,但是版本已经从,1->2->3
3.t2线程根据3秒前拿到的版本进行修改,即使数据值都为100,但是3秒前的版本是1,现在的版本为3,所以修改失败
打印输出:
- 首先t1,t2线程同时拿到对象的最初版本1,值为100
t1第一次版本:1
t2第一次版本:1
t1第一次修改成功?:true
t1第二次版本:2
t1第一次修改成功?:true
t2修改成功?:false
期望版本是:1--当前版本是:3
问题:
一切看上去好像都没什么问题,但是当我们把初始值改成大于127的数值,代码如下,修改的地方已经标注,其实只是将里面的100全部替换为400
public class AtomicStampedReferenceTest {
static AtomicStampedReference<Integer> integerReference =
// 将100改成400
new AtomicStampedReference<>(400,1);
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
int stamp = integerReference.getStamp();
System.out.println("t1第一次版本:"+stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 将100改成400
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(400, 101,
stamp, stamp + 1));
stamp = integerReference.getStamp();
System.out.println("t1第二次版本:"+stamp);
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(101,400,
stamp,stamp+1));
},"t1");
Thread t2 = new Thread(() -> {
int stamp = integerReference.getStamp();
System.out.println("t2第一次版本:"+stamp);
// 睡眠3秒保证t1完成一次ABA操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//将100改成400
System.out.println("t2修改成功?:"+integerReference.compareAndSet(400,500,
stamp,stamp+1));
System.out.println("t2:期望版本是:"+stamp+"--当前版本是:"+integerReference.getStamp());
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
打印输出
t1第一次版本:1
t2第一次版本:1
t1第一次修改成功?:false
t1第二次版本:1
t1第一次修改成功?:false
t2修改成功?:false
t2:期望版本是:1--当前版本是:1
???怎么会呢?我卖100本书让我卖,我卖400本书就不让我卖了?
看看方法
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
// 这一行重点,期望值当前值对比,也就是我们传的100和400
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
- 上面标注的一行为原因所在,这里需要将两个Integer对象进行对比,相等则true否则false,那为啥我们传100就可以400就不行,原因就是Integer的缓存,先看下面的代码与打印输出
Integer a =127;
Integer b =127;
System.out.println(a==b); // 输出true
a=128;
b=128;
System.out.println(a==b);// 输出false
a=-127;
b=-127;
System.out.println(a==b);// 输出true
a=-128;
b=-128;
System.out.println(a==b);// 输出true
a=-129;
b=-129;
System.out.println(a==b);// 输出false
主要原因就是Integer的缓存范围为:[-128,127],Integer用==进行值比较,什么时候相等,什么时候不等?
,即只要你的数值在这个范围内使用上面的方法都没错,但是超过了,则每次修改就会失败
解决方案:使用integer来接收当前的对象,在当前对象的基础上进行修改,即使用100与400的地方都改成通过integerReference.getReference()直接获取当前对象,如果直接传400就会导致Integer装包时创建新的对象,导致数值虽然相等,但是对象不相等。
public class AtomicStampedReferenceTest {
static AtomicStampedReference<Integer> integerReference =
new AtomicStampedReference<>(400,1);
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
int stamp = integerReference.getStamp();
System.out.println("t1第一次版本:"+stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(integerReference.getReference(), 101,
stamp, stamp + 1));
stamp = integerReference.getStamp();
System.out.println("t1第二次版本:"+stamp);
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(integerReference.getReference(),400,
stamp,stamp+1));
},"t1");
Thread t2 = new Thread(() -> {
int stamp = integerReference.getStamp();
System.out.println("t2第一次版本:"+stamp);
// 睡眠3秒保证t1完成一次ABA操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2修改成功?:"+integerReference.compareAndSet(integerReference.getReference(),500,
stamp,stamp+1));
System.out.println("t2:期望版本是:"+stamp+"--当前版本是:"+integerReference.getStamp());
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
网友评论