1.保证i++在多线程下的安全
并发对一个数进行++操作会导致结果不准确,因为++操作本身并不是原子性的,所以在多线程下会出现问题。
/*** @author 二十* @since 2021/8/26 8:53 上午*/public class Demo {private static volatile int count = 0;private static int threadSize = 100;private static CountDownLatch countDownLatch = new CountDownLatch(threadSize);private static void request() {try {TimeUnit.MICROSECONDS.sleep(5);/*** 出现问题的原因:count++分为三步操作,* count->栈 A* B=A+1* count=B*/count++;} catch (InterruptedException e) {e.printStackTrace();}}public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 100; i++) {new Thread(() -> {for (int j = 0; j < 10; j++) {request();}countDownLatch.countDown();}).start();}countDownLatch.await();System.out.println("count:" + count);}}
通过加锁的方式可以保证数据的准确性,但是如果直接在request()加锁,效率会很低,因为这样锁的粒度比较大。
分析一下count++操作:
- count的值赋值给操作数栈内的a
- 将操作数栈内的a+1并赋值给b
- 将操作数栈内b的值赋值给count

并发条件下出现错误的原因就是因为假如a线程在操作一半的过程中(1-3之间),线程b来获取count的值进行++操作,就会获取到不准确的count值。可以控制在第三步加锁,在线程将自己操作数栈内的结果赋值给count之前,先比较当前的最新count和当前线程操作数栈内拷贝的count副本值是否一致,如果一致,则当前线程可以将自己操作数栈的结果赋值给count,否则重新获取count值进行操作,直到成功。
/*** @author 二十* @since 2021/8/26 8:53 上午*/public class Demo {private static volatile int count = 0;private static int threadSize = 100;private static CountDownLatch countDownLatch = new CountDownLatch(threadSize);private static void request() {try {TimeUnit.MICROSECONDS.sleep(5);/*** 出现问题的原因:count++分为三步操作,* count->栈 A* B=A+1* count=B*/// count++;int e;while (!compareAndSwap(e = getCount(), e + 1)) {}} catch (InterruptedException e) {e.printStackTrace();}}private static int getCount() {return count;}private static synchronized boolean compareAndSwap(int e, int n) {if (getCount() == e) {count = n;return true;}return false;}public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 100; i++) {new Thread(() -> {for (int j = 0; j < 10; j++) {request();}countDownLatch.countDown();}).start();}countDownLatch.await();System.out.println("count:" + count);}}
2.Java对cas的支持
2.1 cas概念
CAS 全称“CompareAndSwap”,中文翻译过来为“比较并替换”
CAS操作包含三个操作数————内存位置(V)、期望值(A)和新值(B)。
- 如果内存位置的值与期望值匹配,那么处理器会自动将该位置值更新为新值。
- 否则,处理器不作任何操作。
- 无论哪种情况,它都会在CAS指令之前返回该位置的值。(CAS在一些特殊情况下仅返回CAS是否成功,而不提取当前值)
- CAS有效的说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改。
2.2 jdk中对cas提供的支持
java中提供了对CAS操作的支持,具体在sun.misc.unsafe类中,声明如下:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
| 参数var1 | 表示要操作的对象 |
|---|---|
| 参数var2 | 表示要操作对象中属性地址的偏移量 |
| 参数var4 | 表示需要修改数据的期望的值 |
| 参数var5 | 表示需要修改为的新值 |
2.3 cas实现原理
CAS通过调用JNI的代码实现,JNI:java Native Interface,允许java调用其它语言。而compareAndSwapxxx系列的方法就是借助“C语言”来调用cpu底层指令实现的。
以常用的Intel x86平台来说,最终映射到的cpu的指令为“cmpxchg”,这是一个原子指令,cpu执行此命令时,实现比较并替换的操作!
2.4 cmpxchg怎么保证多核心下的线程安全?
系统底层进行CAS操作的时候,会判断当前系统是否为多核心系统,如果是就给“总线”加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作,也就是说CAS的原子性是平台级别的!
2.5 cas存在的问题
1)ABA问题
线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。
举例:一个小偷,把别人家的钱偷了之后又还了回来,还是原来的钱吗,你老婆出轨之后又回来,还是原来的老婆吗?ABA问题也一样,如果不好好解决就会带来大量的问题。最常见的就是资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律。
2)循环时间长开销大
3)只能保证一个共享变量的原子操作
2.6 ABA问题演示
/*** @author 二十* @since 2021/8/26 2:15 下午*/public class Aba {static CountDownLatch countDownLatch = new CountDownLatch(2);static AtomicInteger i = new AtomicInteger(1);public static void main(String[] args) {new Thread(() -> {int e = Aba.i.get();int n = e + 1;try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException ex) {ex.printStackTrace();}boolean flag = i.compareAndSet(e, n);System.out.println("flag = " + flag);countDownLatch.countDown();}).start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {i.incrementAndGet();i.decrementAndGet();countDownLatch.countDown();}).start();try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}}}
2.7 jdk如何解决ABA问题
解决ABA问题其实很简单,只需要加一个版本号,每次操作,版本号都会加1,每次比较交换的时候,把版本号也比较一下,这样就能确保,这个数据是正确的。
AtomicStampReference主要包含一个对象引用及一个可以自动更新的整数“stamp”的pair对象来解决ABA问题。
/*** @author 二十* @since 2021/8/26 2:29 下午*/public class AtomicStampReferenceTest {static CountDownLatch countDownLatch = new CountDownLatch(2);static AtomicStampedReference<Integer> i = new AtomicStampedReference(1, 1);public static void main(String[] args) {new Thread(() -> {int e = Aba.i.get();int n = e + 1;int ev = i.getStamp();try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException ex) {ex.printStackTrace();}boolean flag = i.compareAndSet(e, n, ev, ev + 1);System.out.println("flag = " + flag);countDownLatch.countDown();}).start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {i.set(i.getReference() + 1, i.getStamp() + 1);i.set(i.getReference() - 1, i.getStamp() + 1);countDownLatch.countDown();}).start();try {countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}}}
