1.保证i++在多线程下的安全

并发对一个数进行++操作会导致结果不准确,因为++操作本身并不是原子性的,所以在多线程下会出现问题。

  1. /**
  2. * @author 二十
  3. * @since 2021/8/26 8:53 上午
  4. */
  5. public class Demo {
  6. private static volatile int count = 0;
  7. private static int threadSize = 100;
  8. private static CountDownLatch countDownLatch = new CountDownLatch(threadSize);
  9. private static void request() {
  10. try {
  11. TimeUnit.MICROSECONDS.sleep(5);
  12. /**
  13. * 出现问题的原因:count++分为三步操作,
  14. * count->栈 A
  15. * B=A+1
  16. * count=B
  17. */
  18. count++;
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. public static void main(String[] args) throws InterruptedException {
  24. for (int i = 0; i < 100; i++) {
  25. new Thread(() -> {
  26. for (int j = 0; j < 10; j++) {
  27. request();
  28. }
  29. countDownLatch.countDown();
  30. }).start();
  31. }
  32. countDownLatch.await();
  33. System.out.println("count:" + count);
  34. }
  35. }

通过加锁的方式可以保证数据的准确性,但是如果直接在request()加锁,效率会很低,因为这样锁的粒度比较大。

分析一下count++操作:

  1. count的值赋值给操作数栈内的a
  2. 将操作数栈内的a+1并赋值给b
  3. 将操作数栈内b的值赋值给count

image.png
并发条件下出现错误的原因就是因为假如a线程在操作一半的过程中(1-3之间),线程b来获取count的值进行++操作,就会获取到不准确的count值。可以控制在第三步加锁,在线程将自己操作数栈内的结果赋值给count之前,先比较当前的最新count和当前线程操作数栈内拷贝的count副本值是否一致,如果一致,则当前线程可以将自己操作数栈的结果赋值给count,否则重新获取count值进行操作,直到成功。

  1. /**
  2. * @author 二十
  3. * @since 2021/8/26 8:53 上午
  4. */
  5. public class Demo {
  6. private static volatile int count = 0;
  7. private static int threadSize = 100;
  8. private static CountDownLatch countDownLatch = new CountDownLatch(threadSize);
  9. private static void request() {
  10. try {
  11. TimeUnit.MICROSECONDS.sleep(5);
  12. /**
  13. * 出现问题的原因:count++分为三步操作,
  14. * count->栈 A
  15. * B=A+1
  16. * count=B
  17. */
  18. // count++;
  19. int e;
  20. while (!compareAndSwap(e = getCount(), e + 1)) {
  21. }
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. private static int getCount() {
  27. return count;
  28. }
  29. private static synchronized boolean compareAndSwap(int e, int n) {
  30. if (getCount() == e) {
  31. count = n;
  32. return true;
  33. }
  34. return false;
  35. }
  36. public static void main(String[] args) throws InterruptedException {
  37. for (int i = 0; i < 100; i++) {
  38. new Thread(() -> {
  39. for (int j = 0; j < 10; j++) {
  40. request();
  41. }
  42. countDownLatch.countDown();
  43. }).start();
  44. }
  45. countDownLatch.await();
  46. System.out.println("count:" + count);
  47. }
  48. }

2.Java对cas的支持

2.1 cas概念

CAS 全称“CompareAndSwap”,中文翻译过来为“比较并替换”

CAS操作包含三个操作数————内存位置(V)、期望值(A)和新值(B)。

  1. 如果内存位置的值与期望值匹配,那么处理器会自动将该位置值更新为新值。
  2. 否则,处理器不作任何操作。
  3. 无论哪种情况,它都会在CAS指令之前返回该位置的值。(CAS在一些特殊情况下仅返回CAS是否成功,而不提取当前值)
  4. CAS有效的说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改。

cas流程jpg.jpg

2.2 jdk中对cas提供的支持


java中提供了对CAS操作的支持,具体在sun.misc.unsafe类中,声明如下:

  1. public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
  2. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
  3. 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问题演示

  1. /**
  2. * @author 二十
  3. * @since 2021/8/26 2:15 下午
  4. */
  5. public class Aba {
  6. static CountDownLatch countDownLatch = new CountDownLatch(2);
  7. static AtomicInteger i = new AtomicInteger(1);
  8. public static void main(String[] args) {
  9. new Thread(() -> {
  10. int e = Aba.i.get();
  11. int n = e + 1;
  12. try {
  13. TimeUnit.SECONDS.sleep(2);
  14. } catch (InterruptedException ex) {
  15. ex.printStackTrace();
  16. }
  17. boolean flag = i.compareAndSet(e, n);
  18. System.out.println("flag = " + flag);
  19. countDownLatch.countDown();
  20. }).start();
  21. try {
  22. TimeUnit.SECONDS.sleep(1);
  23. } catch (InterruptedException e) {
  24. e.printStackTrace();
  25. }
  26. new Thread(() -> {
  27. i.incrementAndGet();
  28. i.decrementAndGet();
  29. countDownLatch.countDown();
  30. }).start();
  31. try {
  32. countDownLatch.await();
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. }

2.7 jdk如何解决ABA问题

解决ABA问题其实很简单,只需要加一个版本号,每次操作,版本号都会加1,每次比较交换的时候,把版本号也比较一下,这样就能确保,这个数据是正确的。

AtomicStampReference主要包含一个对象引用及一个可以自动更新的整数“stamp”的pair对象来解决ABA问题。

  1. /**
  2. * @author 二十
  3. * @since 2021/8/26 2:29 下午
  4. */
  5. public class AtomicStampReferenceTest {
  6. static CountDownLatch countDownLatch = new CountDownLatch(2);
  7. static AtomicStampedReference<Integer> i = new AtomicStampedReference(1, 1);
  8. public static void main(String[] args) {
  9. new Thread(() -> {
  10. int e = Aba.i.get();
  11. int n = e + 1;
  12. int ev = i.getStamp();
  13. try {
  14. TimeUnit.SECONDS.sleep(2);
  15. } catch (InterruptedException ex) {
  16. ex.printStackTrace();
  17. }
  18. boolean flag = i.compareAndSet(e, n, ev, ev + 1);
  19. System.out.println("flag = " + flag);
  20. countDownLatch.countDown();
  21. }).start();
  22. try {
  23. TimeUnit.SECONDS.sleep(1);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. new Thread(() -> {
  28. i.set(i.getReference() + 1, i.getStamp() + 1);
  29. i.set(i.getReference() - 1, i.getStamp() + 1);
  30. countDownLatch.countDown();
  31. }).start();
  32. try {
  33. countDownLatch.await();
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. }