Atomic与Synchronized对比

  1. public class AtomicIntegerDemo {
  2. static Integer i = 0;
  3. static AtomicInteger j = new AtomicInteger(0);
  4. public static void main(String[] args) {
  5. synchronizedAdd();
  6. // atomicAdd();
  7. }
  8. @SuppressWarnings("unused")
  9. private static void synchronizedAdd() {
  10. for(int i = 0; i < 10; i++) {
  11. new Thread() {
  12. public void run() {
  13. // 10个线程就要依次的慢慢的一个一个的进入锁代码块
  14. // 然后依次对i变量进行++的操作
  15. // 每次操作完i++,就写回到主存,下一个线程间进行从主存来加载,再次i++
  16. synchronized(AtomicIntegerDemo.class) {
  17. try {
  18. Random random = new Random();
  19. int time = random.nextInt(3) * 1000;
  20. Thread.sleep(time);
  21. System.out.print("休眠" + time +"ms");
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. System.out.println(" " + ++AtomicIntegerDemo.i);;
  26. }
  27. };
  28. }.start();
  29. }
  30. }
  31. private static void atomicAdd() {
  32. for(int i = 0; i < 10; i++) {
  33. new Thread() {
  34. public void run() {
  35. try {
  36. Random random = new Random();
  37. int time = random.nextInt(3) * 1000;
  38. Thread.sleep(time);
  39. System.out.print("休眠" + time +"ms");
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. System.out.println(" "+AtomicIntegerDemo.j.incrementAndGet());
  44. };
  45. }.start();
  46. }
  47. }
  48. }

先来看一下synchronized的结果是什么样的。可以发现用synchronized显示的结果是串行化的,意味着现在这个多个10个线程的能力压根就没有施展出来,就是一种很low的做法
image.png
再看一下atomic的效果。可以看到这个效果是无序的,但是结果是正确的。也就是意味着多线程的能力发挥出来了,累加代码的这块临界资源是可以被别的线程所访问到,不再是串行化的了。
image.png

Atomic基本原理

image.png

  • 首先线程1开始做操作。
    • 2.线程1读取i=0变量
    • 1.对i变量累加,i=1
  • 随后线程2开始操作
    • 4.线程2读取i=1变量
  • 此时线程3比较快开始操作
    • 6.读取i=1变量
    • 5.与内存中的i变量比对,是否还是1,是的话就继续累加,不是的话就再次读取一下主内存的变量值做累加
  • 终于轮到线程2开始操作
    • 3.开始累积的时候,也要做一下比对,由于线程3把i变量变成2,线程2拿到的还是i=1,线程2就要把这个值比对一下,不一样就要重新拉取i=2再进行累加。

源码剖析

仅限JDK内部使用的Unsafe类

为什么说是仅限JDK内部使用?一步步跟源码可以看到,这个类是私有的,只能通过getUnsafe()方法获取。这里可以通过反编译看到这个getUnsafe这个方法中做了一个判断,获取调用者的class,判断一下这个class的类加载器是否是启动类加载器(Bootstrap)。
image.png->image.png
再来看一下AtomicInteger类为什么就保证别的线程都读的是最新的值?首先,这个value是被volatile修饰的,传进的值通过最简单的方式给value复制,当一个线程对这个修改了之后别的线程也就会看到修改后的值了。
image.png

原子类核心操作之无限循环以及cas操作

  1. private static final long valueOffset;
  2. static {
  3. try {
  4. valueOffset = unsafe.objectFieldOffset
  5. (AtomicInteger.class.getDeclaredField("value"));
  6. } catch (Exception ex) { throw new Error(ex); }
  7. }

类初始化的时候做了一些事情,就是找到传经来的这个value这个字段具体是在AtomicInterger这个类的那个位置,offset偏移量,通过unsafe类来实现,类初始化的时候就会完成这个操作,同时这个类也是被final所修饰,初始化完成后就不再变化了。

  1. public final int incrementAndGet() {
  2. return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
  3. }
  4. public final int getAndAddInt(Object var1, long var2, int var4) {
  5. int var5;
  6. do {
  7. //获取当前AtomicInteger对象实例,
  8. //以及传进去value的偏移量,通过这两个参数获取到value的值
  9. var5 = this.getIntVolatile(var1(this), var2(valueOffset));
  10. }
  11. //2
  12. while(!this.compareAndSwapInt(var1(this), var2(valueOffset), var5(value), var5(value) + var4(1)));
  13. return var5;
  14. }
  1. 获取当前AtomicInteger对象实例,以及传进去value的偏移量,通过这两个参数获取到value的值。
  2. 拿到刚才的value,去跟AtomicInteger对象实例的value去比较一下。
  • 如果比对的值不一样,while循环里拿到的是false就会进入下一轮的循环。
  • 如果是一样的话就把value的值设置为:之前拿到的值加一(var5(value) + var4(1))。同时会返回一个value的值,这个是在递增前的一个旧值,现在可以看一下外层的方法就会把这个旧值给加1然后返回,就得到一个正确的值。return unsafe.getAndAddInt(this, valueOffset, 1) + 1;

第二步中标黄部分,为什么这里要用旧值累加一返回,而不是根据偏移量直接获取到AtomicInteger中value的值做返回呢?
image.png
图中箭头指向的地方,这块如果再调用一次这个方法=>var5 = this.getIntVolatile(var1(this), var2(valueOffset));。由于线程A的cas操作已经完成了,线程B就可以开始cas了,线程B在cas完成之后,这个value值就变了。就有可能,线程A在这里调用再次获取值的方法时就拿到了线程Bcas后的value值,用这个值再返回那就不对了。因此,就用原值做一下返回再加一。此时就不用考虑什么原子性不原子性了,因为底层已经用cas操作过了,值一定是对的,现在主要是要返回一个当前线程操作过的正确的值就行了。

CAS语义存在的三大缺点

ABA问题

假设一开始变量i = 1,你先获取这个i的值是1,然后准备累加了1的时候。但是在此期间,别的线程将i -> 1 -> 2 -> 3 -> 1,把这个1做了系列的操作又变成了1,此时线程A去拿着这个1和之前获取的1比较,乍一看1都是一样的直接累加吧没什么问题。实际上此1非彼1,可能会有一些问题。
所以atomic包里有AtomicStampedReference类,就是会比较两个值的引用是否一致,如果一致,才会设置新值
用AtomicInteger,常见的是计数,所以说一般是不断累加的,所以ABA问题比较少见。

无限循环问题

image.png
从源码中就能看到这里有个循环,极端情况下,当前这个线程就是点背就是cas设置不了,那么他就会一直在这个doWhile循环中轮询。这个在高并发修改一个值的时候其实挺常见的,比如你用AtomicInteger在内存里搞一个原子变量,然后高并发下,多线程频繁修改,其实可能会导致这个compareAndSet()里要循环N次才设置成功,所以还是要考虑到的。
JDK 1.8引入的LongAdder来解决,是一个重点,分段CAS思路。

多变量原子问题

一般的AtomicInteger,只能保证一个变量的原子性,但是如果多个变量呢。
可以用AtomicReference,这个是封装自定义对象的,多个变量可以放一个自定义对象里,然后他会检查这个对象的引用是不是一个。