亮,有一计

文章 分类 评论
49 17 9

站点介绍

这是一个热爱和别人一起交流学习的程序员所建立的个人博客 欢迎大家交流讨论

原子操作18个类详解

liang 2022-07-11 162 0条评论 java锁多线程知识juc多线程编码学习 CAS原子操作

首页 / 正文
本站的文章若无特殊标明转载的话均为原创,有些文章在我的csdn个人站也有,两者基本上是同步的 博客地址

发布于2022-07-08

原子操作18个类详解

基本类型原子类

常用api列表
  • public final int get() //获取当前的值
  • public final int getAndSet(int newValue)//获取当前的值,并设置新的值
  • public final int getAndIncrement()//获取当前的值,并自增
  • public final int getAndDecrement() //获取当前的值,并自减
  • public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
  • boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)

    结合countDownLatch案例
    static class MyNumber {
          AtomicInteger atomicInteger = new AtomicInteger(0);
    
          public void addPlus() {
              atomicInteger.getAndIncrement();
          }
      }
    
      static int SIZE = 50;
    
      public static void main(String[] args) throws InterruptedException {
          MyNumber myNumber = new MyNumber();
          CountDownLatch countDownLatch = new CountDownLatch(50);
          for (int i = 0; i < SIZE; i++) {
              new Thread(()->{
                  try {
                      for (int j = 0; j < 100; j++) {
                          myNumber.addPlus();
                      }
                  }finally {
                      countDownLatch.countDown();
                  }
    
              }).start();
          }
          countDownLatch.await();
          System.out.println(myNumber.atomicInteger.get());
      }

    数组类型的原子类

    操作方法都需要传入数组的下标值 构造的时候不赋值默认就是基本类型的初始值,比如int类型就是0

    AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
          //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
          //AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});
    
          for (int i = 0; i <atomicIntegerArray.length(); i++) {
              System.out.println(atomicIntegerArray.get(i));
          }

    引用类型原子类

    引用类和邮戳类

    CAS的基础介绍

    标记类AtomicMarkableReference

    解决引用对象是否修改过的问题

    static AtomicMarkableReference<Integer> markableReference = new AtomicMarkableReference<>(100,false);
    System.out.println("============AtomicMarkableReference不关心引用变量更改过几次,只关心是否更改过======================");
    
          new Thread(() -> {
              boolean marked = markableReference.isMarked();
              System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);
              try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
              markableReference.compareAndSet(100,101,marked,!marked);
              System.out.println(Thread.currentThread().getName()+"\t 2次版本号"+markableReference.isMarked());
              markableReference.compareAndSet(101,100,markableReference.isMarked(),!markableReference.isMarked());
              System.out.println(Thread.currentThread().getName()+"\t 3次版本号"+markableReference.isMarked());
          },"t5").start();
    
          new Thread(() -> {
              boolean marked = markableReference.isMarked();
              System.out.println(Thread.currentThread().getName()+"\t 1次版本号"+marked);
              //暂停几秒钟线程
              try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
              markableReference.compareAndSet(100,2020,marked,!marked);
              System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference()+"\t"+markableReference.isMarked());
          },"t6").start();

    对象的属性修改原子类

    达成的效果: 以一种线程安全的方式操作非线程安全对象内的某些字段

    使用的要求:

    • 更新的对象属性必须使用 public volatile 修饰符。
    • 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
    AtomicIntegerFieldUpdater
    class BankAccount
    {
      private String bankName = "CCB";//银行
      public volatile int money = 0;//钱数
      AtomicIntegerFieldUpdater<BankAccount> accountAtomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");
    
      //不加锁+性能高,局部微创
      public void transferMoney(BankAccount bankAccount)
      {
          accountAtomicIntegerFieldUpdater.incrementAndGet(bankAccount);
      }
    }
    
    /**
     * @auther zzyy
     * @create 2020-07-14 18:06
     * 以一种线程安全的方式操作非线程安全对象的某些字段。
     * 需求:
     * 1000个人同时向一个账号转账一元钱,那么累计应该增加1000元,
     * 除了synchronized和CAS,还可以使用AtomicIntegerFieldUpdater来实现。
     */
    public class AtomicIntegerFieldUpdaterDemo
    {
    
      public static void main(String[] args)
      {
          BankAccount bankAccount = new BankAccount();
    
          for (int i = 1; i <=1000; i++) {
              int finalI = i;
              new Thread(() -> {
                  bankAccount.transferMoney(bankAccount);
              },String.valueOf(i)).start();
          }
    
          //暂停毫秒
          try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
    
          System.out.println(bankAccount.money);
    
      }
    }
    AtomicReferenceFieldUpdater
    class MyVar
    {
      public volatile Boolean isInit = Boolean.FALSE;
      AtomicReferenceFieldUpdater<MyVar,Boolean> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");
    
    
      public void init(MyVar myVar)
      {
          if(atomicReferenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE))
          {
              System.out.println(Thread.currentThread().getName()+"\t"+"---init.....");
              //暂停几秒钟线程
              try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
              System.out.println(Thread.currentThread().getName()+"\t"+"---init.....over");
          }else{
              System.out.println(Thread.currentThread().getName()+"\t"+"------其它线程正在初始化");
          }
      }
    
    
    }
    
    
    /**
     * 多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化工作,要求只能初始化一次
     */
    public class AtomicIntegerFieldUpdaterDemo
    {
      public static void main(String[] args) throws InterruptedException
      {
          MyVar myVar = new MyVar();
    
          for (int i = 1; i <=5; i++) {
              new Thread(() -> {
                  myVar.init(myVar);
              },String.valueOf(i)).start();
          }
      }
    }
     

    原子操作增强类

    LongAdder与LongAccumulator

    LongAdder必须从0开始,而且只有加减法 具有非常大的局限性,但是LongAccumulator是调用的函数式接口,加减乘除等等运算符号随便自定义,并且能设置初始值,非常灵活

          LongAdder longAdder = new LongAdder();
          longAdder.decrement();
          longAdder.decrement();
          longAdder.decrement();
          System.out.println(longAdder.longValue());
          LongAccumulator longAccumulator = new LongAccumulator((Long::sum), 1);
          longAccumulator.accumulate(1);
          longAccumulator.accumulate(2);
          longAccumulator.accumulate(3);
          System.out.println(longAccumulator.longValue());

    LongAdder为什么快

    父类Striped64

    重要的成员

    /** Number of CPUS, to place bound on table size        CPU数量,即cells数组的最大长度 */
    static final int NCPU = Runtime.getRuntime().availableProcessors();
    
    /**
     * Table of cells. When non-null, size is a power of 2.
    cells数组,为2的幂,2,4,8,16.....,方便以后位运算
     */
    transient volatile Cell[] cells;
    
    /**基础value值,当并发较低时,只累加该值主要用于没有竞争的情况,通过CAS更新。
     * Base value, used mainly when there is no contention, but also as
     * a fallback during table initialization races. Updated via CAS.
     */
    transient volatile long base;
    
    /**创建或者扩容Cells数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁。
     * Spinlock (locked via CAS) used when resizing and/or creating Cells. 
     */
    transient volatile int cellsBusy;

    Striped64重要的成员介绍

    设计思想

    LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
    sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点

    LongAdder在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。

    LongAdder源码分析

    add方法解析

    public void add(long x) {
          //as是Striped64中的cells数组
          //b是Striped64中的base属性
          //v是当前线程hash到Cell中存储的值
          //m是cells的长度减1,hash时作为掩码使用
          //a是当前线程hash到的cell
          Striped64.Cell[] as; long b, v; int m; Striped64.Cell a;
          //第一个线程首次进来(as = cells) != null一定是false的,此时走casBase方法,以cas的方式更新base值,且只有当cas失败时,才会走if分支
          //条件1:cells不为空,说明出现过竞争。Cell[]数组已经创建
          //条件2:cas操作base失败,说明其他线程先一步修改了base正在出现竞争
          if ((as = cells) != null || !casBase(b = base, b + x)) {
              //true无竞争 false表示竞争激烈,多个线程hash到同一个cell。可能要扩容
              boolean uncontended = true;
              //条件1:cells为空,说明正在出现竞争,上面是从条件2过来的
              //条件2:应该不会出现
              //条件3:当前线程所在的cell为空,说明当前线程还没更新过cell,应该初始化一个cell
              //条件4:更新当前线程所在的cell失败,说明现在线程竞争激烈,多个线程hash到了同一个cell,应该进行扩容了
              if (as == null || (m = as.length - 1) < 0 ||
                      //getProbe方法返回的是线程中的threadLocalRandomProbe字段
                      //他是通过随机数生成的一个值,对于一个确定的线程这个值是固定的(除非特意修改过)
                      (a = as[getProbe() & m]) == null ||
                      !(uncontended = a.cas(v = a.value, v + x)))
                  longAccumulate(x, null, uncontended);
          }
      }

    longAccumulate方法解析

    线程hash值:getProbe()方法
    final void longAccumulate(long x, LongBinaryOperator fn,
                                boolean wasUncontended) {
          //存储线程的PROBE值
          int h;
          //如果getProbe()方法返回0,说明随机数未初始化
          if ((h = getProbe()) == 0) {
              //使用ThreadLocalRandom为当前线程重新计算一个hash值,强制他进行初始化
              ThreadLocalRandom.current(); // force initialization
              //重新获取强制初始化后的值
              h = getProbe();
              //重新计算了当前线程的hash后认为此次不算是一次竞争,都未初始化,肯定不是激烈的竞争,所以把wasUncontended设置为true
              wasUncontended = true;
          }
      }
      
    static final int getProbe() {
          return UNSAFE.getInt(Thread.currentThread(), PROBE);
      }
    
     private static final sun.misc.Unsafe UNSAFE;
      private static final long BASE;
      private static final long CELLSBUSY;
      private static final long PROBE;
      static {
          try {
              UNSAFE = sun.misc.Unsafe.getUnsafe();
              Class<?> sk = Striped64.class;
              BASE = UNSAFE.objectFieldOffset
                  (sk.getDeclaredField("base"));
              CELLSBUSY = UNSAFE.objectFieldOffset
                  (sk.getDeclaredField("cellsBusy"));
              Class<?> tk = Thread.class;
              PROBE = UNSAFE.objectFieldOffset
                  (tk.getDeclaredField("threadLocalRandomProbe"));
          } catch (Exception e) {
              throw new Error(e);
          }
      }
      /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
      @sun.misc.Contended("tlr")
      int threadLocalRandomProbe;
    分路解析(代码的三个分路)

    上述代码首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:

    • 分路1:Cell[]数组已经初始化
    • 分路2:Cell[]数组未初始化(首次新建)
    • 分路3:Cell[]数组正在初始化中(注意这是个兜底措施,最后实在不行就继续去找base)
      一直到有正确的处理后才会break跳出这个死循环
    final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
          // 如果hash取模映射得到的Cell单元不是null则为true,此值也可以看做是扩容意向
          boolean collide = false;                
          for (;;) {
              Striped64.Cell[] as; Striped64.Cell a; int n; long v;
              //分路1: cells数组已经被初始化了
              if ((as = cells) != null && (n = as.length) > 0) {}
              //分路2:cells没有加锁且没有初始化,则尝试对他进行加锁,并去初始化cells数组
              else if (cellsBusy == 0 && cells == as && casCellsBusy()) {}
              //分路3: cells正在进行初始化,则直接尝试在基数base上进行累加操作
              else if (casBase(v = base, ((fn == null) ? v + x :
                      fn.applyAsLong(v, x))))
          // 注意这里 分路3是最后的兜底措施,要是兜底也没管用就继续这个死循环走下去一直到有正常地处理为止
                  break;     
          }
      }
    分路2解析(初始化cell数组)
    如果上面条件都执行成功就会执行数组的初始化及赋值操作, Cell[] rs = new Cell[2]表示数组的长度为2,
    rs[h & 1] = new Cell(x) 表示创建一个新的Cell元素,value是x值,默认为1。
    h & 1类似于我们之前HashMap常用到的计算散列桶index的算法,通常都是hash & (table.len - 1)。同hashmap是一个意思。
    //cells == as的一个双端检索的机制
      else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
          boolean init = false;
          try {                           // Initialize table
              if (cells == as) {
                  //新建一个大小为2的Cell数组 后面扩容也是2的倍数
                  Striped64.Cell[] rs = new Striped64.Cell[2];
                  rs[h & 1] = new Striped64.Cell(x);
                  cells = rs;
                  init = true;
              }
          } finally {
              cellsBusy = 0;
          }
          if (init)
              break;
      }
    分路1解析(是否扩容和cell数组上的cas)
    上面代码判断当前线程hash后指向的数据位置元素是否为空,如果为空则将Cell数据放入数组中,跳出循环。如果不空则继续循环。
    //当前线程的hash值运算后映射得到的Cell对象为null 说明该cell没有被使用
      if ((a = as[(n - 1) & h]) == null) {
          if (cellsBusy == 0) {       // cell数组没有正在扩容
              Striped64.Cell r = new Striped64.Cell(x);   // 创建一个新的cell对象
              if (cellsBusy == 0 && casCellsBusy()) {   //尝试加锁,成功后cellBusy置为1
                  boolean created = false;
                  try {               // 在有锁的情况下再检测一遍之前的判断
                      Striped64.Cell[] rs; int m, j; //将cell对象赋值到cells数组上
                      if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) {
                          rs[j] = r;
                          created = true;
                      }
                  } finally {
                      cellsBusy = 0;
                  }
                  if (created)
                      break;
                  continue;           // Slot is now non-empty
              }
          }
          collide = false;
      }
    说明当前线程对应的数组中有了数据,也重置过hash值,这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS成功则直接跳出循环。
    else if (a.cas(v = a.value, ((fn == null) ? v + x :
                                               fn.applyAsLong(v, x))))
                      break;
    如果扩容的数组要大于cpu的数量了 就不允许在扩容了
    else if (n >= NCPU || cells != as)
                      collide = false;   
    进行cells数组的扩容
    else if (cellsBusy == 0 && casCellsBusy()) {
          try { //当前的cells数组和最先赋值的as是同一个,代表没有被其他线程扩容过
              if (cells == as) {      // 左移一位,扩容大小为之前的两倍
                  Striped64.Cell[] rs = new Striped64.Cell[n << 1];
                  for (int i = 0; i < n; ++i)
                      rs[i] = as[i];   //扩容后把之前的元素放到新数组中
                  cells = rs;
              }
          } finally {
              cellsBusy = 0;  //释放锁,设置扩容状态,然后继续去循环 把这次的+1操作计数完成
          }
          collide = false;
          continue;                   // Retry with expanded table
      }
    每次循环都会对当前线程的hash值进行清空
    h = advanceProbe(h);
    static final int advanceProbe(int probe) {
          probe ^= probe << 13;   // xorshift
          probe ^= probe >>> 17;
          probe ^= probe << 5;
          UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
          return probe;
      }

    总结

    LongAdder的add方法总结

    最终总结

    atomic原子类

  • 通过CAS自旋实现
  • 线程安全,可允许一些性能损耗,要求高精度时可使用
  • 保证精度,性能代价
  • AtomicLong是多个线程针对单个热点值value进行原子操作
  • 高并发后性能急剧下降,大量线程自旋,cpu空转
  • LongAdder和LongAccumulator

  • CAS+Base+Cell数组分散
  • 空间换时间并分散了热点数据
  • 当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
  • 保证性能,精度代价(他只保证最终一致性,sum()方法不保证实时的一致性)
  • LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作

评论(0)

最新评论

  • Hello Word !

    我发现我的评论功能,加载不了头像!朋友圈和相册也加载不了,音乐播放器也不显示,求解!

  • il: Clark11370

    Margenemh 357 fotballdrakter til bedriftslag Coreyc – Coin Media – журнал о заработке в интернете Francesco CasieDalu maglia argentina AbbieLand JessRoman Website Darrellie PaulinaRu Profile of ChristieRy SidneyMgw ToneyKrog Liverpool Tøj - Blog Profile - iotaJots JaimieChe TriciaBri Wolearn: GregoryEmelia: Magliette Calcio Poco Prezzo SheltonTh WayneHone 438 Chile Tröja Barn Leopol – Profile – Primescool Forum CharlineC VitoHartf norge bortedrakt to receive adulation, always insists the result is most important and will therefore be thoroughly disheartened by this display. As is often the case in friendly matches, there was a lack FedericoW صفحه اصلی – Maglia Porto KurtBrise of urgency for large periods during this match, though Wales did link play promisingly in attack through Wilson and Brooks. Italy will be among the nations in pot one for the Euro 2020 qualifying draw in Dublin on 2 December having drawn 0-0 against Portugal in their previous match to ensure a second-place finish in Nations League Group 3A. TanjaFitz Elizabeth manchester city tröjor KenPoulin CarenWhar Maglia Juventus Bambino EmeliaBow Stephaine liverpool drakt DarlaFulm FerneSuvg barcelona FC drakt Meaganold EmilyEise arsenal drakt KourtneyS Kellyejoh inter trøje JamaalWor Loisniyet psg drakt MaricelaS CecileSwa lavora come produttore esecutivo della serie CharissaC ChastityF Home - Liverpool Drakt EmilGmvut CasieDalu maglia argentina AbbieLand UrsulaLof rashford numero maglia KareemCap SSNGilber arsenal drakt 2022 MelodeePe

  • arsenal trøjer

    BrittnyFo atletico madrid tröja ClaritaBu ZJRBridge magliette psg JosephDur Valentina real madrid drakt MarionTol NevaLangt manchester united drakt BraydenBo RodolfoCa Real Madrid Tröja KelliefjL BettinaMi Notenbrood op top niveau MillardMh RichardMo arsenal drakt Bradleyjo TanyaNuge Spanien landsholdstrøje AprilBuck JoannaShe Billige Polen Fotballdrakter NiklasWes Maryellen terza maglia milan PrincessN AnhGiffen real madrid tröja FredricAu JoannaShe Billige Polen Fotballdrakter NiklasWes EssieOate juventus matchtröja PreciousF EdnaTippe sabinahogan447 EffieOcon MichaelaH liverpool tröja barn KayleighM JonnieHoc nuova maglia roma 2021/22 CortezSha YDZClaire ac milan drakt KathleneH CedricBac manchester united drakt Peterysjl HaicoQcmw Maglia Brasile Femmina ThereseJu DoreenMes napoli trøje JadaLanni

  • maglie hellas verona 2022

    Your style is very unique in comparison to other folks I have read stuff from. Many thanks for posting when you have the opportunity, Guess I'll just bookmark this blog. maglie hellas verona 2022 IlanaGxrn frankrike tröja JaneenZtt

  • inter milan tröja

    I truly love your site.. Pleasant colors & theme. Did you make this website yourself? Please reply back as I'm hoping to create my own website and would like to learn where you got this from or just what the theme is named. Cheers! inter milan tröja LilianaCz Frankrike Tröja Barn NganRjpfi

日历

2022年12月

    123
45678910
11121314151617
18192021222324
25262728293031

友情链接

文章目录

推荐关键字:

站点公告
本站的文章若无特殊标明转载的话均为原创,有些文章在我的csdn个人站也有,两者基本上是同步的 博客地址
点击小铃铛关闭