• 【并发编程】内存,volatiile,cas,以及原子类包


    一。可见性

      内存分为主内存和线程内存,当线程开始时会把主内存需要的数据放到线程内存里,但线程内存的数据不会直接返回给主内存,这导致了多线程的情况下,对一个 数据不能做到即使反馈。

    二。volatile

      它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存。

    三。指令重排

      JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。

      volatile 修饰的变量,可以禁用指令重排。

      被volatile修饰的变量在其读操作时产生读屏障,之后的代码不会排在其之前,在写操作时,其它代码不能排到它之后。

    四。cas

      cas全称为compareAndSet 比较并交换,是一个原子操作,在类unsafe在的方法。

      cas必须和volatile配合。

      cas特点:

      结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

      CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再 重试呗。

      synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想 改,我改完了解开锁,你们才有机会。

      CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思。

        因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。

        但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。

     五。原子整数

      AtomicBoolean  ,AtomicInteger ,AtomicLong,以AtomicInteger为例:

    AtomicInteger i = new AtomicInteger(0);
    // 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
    System.out.println(i.getAndIncrement());
    // 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
    System.out.println(i.incrementAndGet());
    // 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
    System.out.println(i.decrementAndGet());
    // 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
    System.out.println(i.getAndDecrement());
    // 获取并加值(i = 0, 结果 i = 5, 返回 0)
    System.out.println(i.getAndAdd(5));
    // 加值并获取(i = 5, 结果 i = 0, 返回 0)
    System.out.println(i.addAndGet(-5));
    // 获取并更新(i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0)
    // 其中函数中的操作能保证原子,但函数需要无副作用
    System.out.println(i.getAndUpdate(p -> p - 2));
    // 更新并获取(i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0)
    // 其中函数中的操作能保证原子,但函数需要无副作用
    System.out.println(i.updateAndGet(p -> p + 2));
    // 获取并计算(i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0)
    // 其中函数中的操作能保证原子,但函数需要无副作用
    // getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的
    // getAndAccumulate 可以通过 参数1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final
    System.out.println(i.getAndAccumulate(10, (p, x) -> p + x));
    // 计算并获取(i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0)
    // 其中函数中的操作能保证原子,但函数需要无副作用
    System.out.println(i.accumulateAndGet(-10, (p, x) -> p + x));

    六。原子引用

      当juc下的原子类不满足于用户需求时,就需要原子引用,来保证操作的原子性,比如BigDecimal

      这时候可以用基于cas理论的原子引用来包装基础类。

      AtomicReference AtomicMarkableReference AtomicStampedReference

    class DecimalAccountSafeCas implements DecimalAccount {
     AtomicReference<BigDecimal> ref;
    public DecimalAccountSafeCas(BigDecimal balance) {   ref = new AtomicReference<>(balance); }

    public BigDecimal getBalance() {
      return ref.get();
    }
    @Override
    public void withdraw(BigDecimal amount) {
        while (true) {
          BigDecimal prev = ref.get();
          BigDecimal next = prev.subtract(amount);
          if (ref.compareAndSet(prev, next)) {
            break;
          }
        }
      }
    }

      当你所操作的数据对版本敏感需要对每个操作都标记,可以使用带版本号的AtomicStampedReference,

    static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    public static void main(String[] args) throws InterruptedException {
     log.debug("main start...");
     // 获取值 A
     String prev = ref.getReference();
     // 获取版本号
     int stamp = ref.getStamp();
     log.debug("版本 {}", stamp);
     // 如果中间有其它线程干扰,发生了 ABA 现象
     other();
     sleep(1);
     // 尝试改为 C
     log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
    }
    private static void other() {
     new Thread(() -> {
     log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
     ref.getStamp(), ref.getStamp() + 1));
     log.debug("更新版本为 {}", ref.getStamp());
     }, "t1").start();
     sleep(0.5);
     new Thread(() -> {
     log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
     ref.getStamp(), ref.getStamp() + 1));
     log.debug("更新版本为 {}", ref.getStamp());
     }, "t2").start();
    }

      相反,若对版本号不敏感,并不关心引用变量更改了几次,只是单纯的关心是否更改过,可以使用 AtomicMarkableReference

    class GarbageBag {
     String desc;
     public GarbageBag(String desc) {
     this.desc = desc;
     }
     public void setDesc(String desc) {
     this.desc = desc;
     }
     @Override
     public String toString() {
     return super.toString() + " " + desc;
     }
    }
    
    @Slf4j
    public class TestABAAtomicMarkableReference {
     public static void main(String[] args) throws InterruptedException {
     GarbageBag bag = new GarbageBag("装满了垃圾");
     // 参数2 mark 可以看作一个标记,表示垃圾袋满了
     AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
     log.debug("主线程 start...");
     GarbageBag prev = ref.getReference();
     log.debug(prev.toString());
     new Thread(() -> {
     log.debug("打扫卫生的线程 start...");
     bag.setDesc("空垃圾袋");
     while (!ref.compareAndSet(bag, bag, true, false)) {}
     log.debug(bag.toString());
     }).start();
     Thread.sleep(1000);
     log.debug("主线程想换一只新垃圾袋?");
     boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
     log.debug("换了么?" + success);
     log.debug(ref.getReference().toString());
     }
    }

    七。原子数组

       AtomicIntegerArray AtomicLongArray AtomicReferenceArray

       数组的使用和原子数子的使用相差不多,而引用数组可以处理除了integer, 和long类型之外的数据

    demo(
     ()-> new AtomicIntegerArray(10),
     (array) -> array.length(),
     (array, index) -> array.getAndIncrement(index),
     array -> System.out.println(array)
    );

     八。字段更新器

      AtomicReferenceFieldUpdater // 域 字段

      AtomicIntegerFieldUpdater

      AtomicLongFieldUpdater  

      利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现 异常

    public class Test5 {
     private volatile int field;
     public static void main(String[] args) {
     AtomicIntegerFieldUpdater fieldUpdater =AtomicIntegerFieldUpdater.newUpdater(Test5.class, "field");
     Test5 test5 = new Test5();
     fieldUpdater.compareAndSet(test5, 0, 10);
     // 修改成功 field = 10
     System.out.println(test5.field);
     // 修改成功 field = 20
     fieldUpdater.compareAndSet(test5, 10, 20);
     System.out.println(test5.field);
     // 修改失败 field = 20
     fieldUpdater.compareAndSet(test5, 10, 30);
     System.out.println(test5.field);
     }
    }

    九。原子累加器

       LongAdder,它的效率比原子整数累加的效率要高很多,性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失败,从而提高性 能。

         关键参数如下:

    // 累加单元数组, 懒惰初始化
    transient volatile Cell[] cells;
    // 基础值, 如果没有竞争, 则用 cas 累加这个域
    transient volatile long base;
    // 在 cells 创建或扩容时, 置为 1, 表示加锁
    transient volatile int cellsBusy;

      流程如下:

      首先累加的线程,通过cas操作累加到base上。

      如果cas操作失败,则会通过创建cell并进行累加,

      cell在添加或者扩容的时候都会添加锁cellsbusy,获取并修改时不会,但也不会产生并发问题,是因为cell开启了伪共享。

      因为 CPU 与 内存的速度差异很大,需要靠预读数据至缓存来提升效率。 而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是 64 byte(8 个 long) 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中 CPU 要保证数据的一致性,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效。

       因为 Cell 是数组形式,在内存中是连续存储的,一个 Cell 为 24 字节(16 字节的对象头和 8 字节的 value),因 此缓存行可以存下 2 个的 Cell 对象。这样问题来了:

       Core-0 要修改 Cell[0]

      Core-1 要修改 Cell[1]

      无论谁修改成功,都会导致对方 Core 的缓存行失效,比如 Core-0 中 Cell[0]=6000, Cell[1]=8000 要累加 Cell[0]=6001, Cell[1]=8000 ,这时会让 Core-1 的缓存行失效

      @sun.misc.Contended 用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding,从而让 CPU 将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

       当累加时的流程是这样的:

    final void longAccumulate(long x, LongBinaryOperator fn,
     boolean wasUncontended) {
     int h;
     // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell
     if ((h = getProbe()) == 0) {
     // 初始化 probe
     ThreadLocalRandom.current();
     // h 对应新的 probe 值, 用来对应 cell
     h = getProbe();
     wasUncontended = true;
     }
     // collide 为 true 表示需要扩容
     boolean collide = false;
     for (;;) {
     Cell[] as; Cell a; int n; long v;
     // 已经有了 cells
     if ((as = cells) != null && (n = as.length) > 0) {
     // 还没有 cell
    if ((a = as[(n - 1) & h]) == null) {
     // 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x
     // 成功则 break, 否则继续 continue 循环
     }
     // 有竞争, 改变线程对应的 cell 来重试 cas
     else if (!wasUncontended)
     wasUncontended = true;
     // cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null
     else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
     break;
     // 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas
     else if (n >= NCPU || cells != as)
     collide = false;
     // 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了
     else if (!collide)
     collide = true;
     // 加锁
     else if (cellsBusy == 0 && casCellsBusy()) {
     // 加锁成功, 扩容
     continue;
     }
     // 改变线程对应的 cell
     h = advanceProbe(h);
     }
     // 还没有 cells, 尝试给 cellsBusy 加锁
     else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
     // 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell
     // 成功则 break;
     }
     // 上两种情况失败, 尝试给 base 累加
     else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
     break;
     }
    }

      当cells列表不存在时的流程如下:

       若cells存在而对应的cell不存在时

       每个线程刚进入 longAccumulate 时,会尝试对应一个 cell 对象(找到一个坑位

       最终通过sum来获取结果:

    public long sum() {
     Cell[] as = cells; Cell a;
     long sum = base;
     if (as != null) {
     for (int i = 0; i < as.length; ++i) {
     if ((a = as[i]) != null)
     sum += a.value;
     }
     }
     return sum;
    }

      值得注意的是,在加的过程中并没有添加锁操作,所以累加器只可以保证最终一致性

  • 相关阅读:
    vue双向绑定的时候把遍历的数组转为了字符串,并且再转回去数组进行绑定
    使用for of循环遍历获取的nodeList,配置babel编译,webpack打包之后在iphone5下报错
    iview表单验证不生效问题注意点
    babel配置项目目录支持转换es6语法,引入非项目目录js后,引入Js转换无效
    swiper4自动轮播切换手动触碰后停止踩坑——属性disableOnInteraction
    移动网页广告引入mraid.js使用指南
    react项目和next项目修改默认端口号
    windows安装mongodb服务简洁版教程
    Fiddler抓包手机代理配置
    手机配置代理报错invalid host header
  • 原文地址:https://www.cnblogs.com/LZXlzmmddtm/p/16273420.html
Copyright © 2020-2023  润新知