贝因美:与国内区块链公司联合探索行业应用 实现精准溯源

今天我们的内容是CAS以及原子操作类应用与源码浅析,还会利用CAS来完成一个单例模式,还涉及到伪共享等。因为CAS是并发框架的基石,所以相当重要,这篇博客是一个长文,请做好准备。

说到CAS,不得不提到两个专业词语:悲观锁,乐观锁。我们先来看看什么是悲观锁,什么是乐观锁。

悲观锁,乐观锁

第一次看到悲观锁,乐观锁的时候,应该是在应付面试,看面试题的时候。有这么一个例子:如何避% ? B I % L w B R免多线程对数) & { H n [据库中的同一条记录进行修改。

悲观锁

如果是mysi l 5 ?ql数据库,利用for updats c `e关键字+事务。这样的效果就是当A线程走L t D g到for update的时候,会把指定的记录上锁,然后B线程过来,就只能等待,A线程修改完数据之后,提交事务,锁就被释放了,这个时候B线程终于可以继续做他的事情了。悲观锁往往是互斥的:只Q B 4 ~ n C T w 6有我一个人可以进来,其他人都给我等着。这么做是相当影响性能的。

乐观锁

在数据表中加一个版本号的字段:version,这个字段不需R O r要程序员手动维护,是数据库主动维护的,每次修改数据,version都会发生更改。

当v` 8 r p U !ersion现在是1:

  • A线程进来,读到version是1。
  • B线程进来,读到version是1。
  • AI 5 g Y线程执行了Y p e 4 a M |Y & x : * m C % D新的操作:update stu set name=\'codebear\' where id=1 and version=1。成功。数据库主动把version改成了2。
  • B| % b r H U Y线程执行了更新的操作:update stu set name=\'hello\' where id=1 and version=1。失败。因为这个时候version字段已经不是1了。

乐观锁其实不能叫锁: ( t _ ? / h,它没有锁的概念。

在Java中,也有悲观锁,乐观锁的概念,悲观锁的典型代表就是Synchronized,而乐观锁的典型代表就是今天要说的CAS。而说CAS之前,2 } * P _先要说下原子操作类,因为CAS是原子操作类的基石,我们先要看看原子操作类的强大之处,从而c W ( P 9 k I @ |产生探究CAS的兴趣。

原子操作类的应用

我们先来看看原子操作类的应用。在Java中提供了很多原子操作类,比如AtomicInteger,其中有一个自增方法。

publicd C + F class R N _ BMain {
    public statM @ F _ )ic void G 4 L k y S i Emain(S/ J k $ o J 1tring[] args) {
       &nb* f W psp;Thread[b X * r y X l N] threads = new&nb= 3 . q 2 R ` T ;sp;Thread[20];
 &nb? ! c g @sp;  &nbD g S Y 7 E isp;   AtomicInteger atomicInteger = q O # & x _ w ` bnew AtomicInteger();
&; ) % o . u % Vnbg ` esp;     &x O g $ k 8 , J ynbsp; for (int i = 0; i < 20; i++) {
        G x Y =;    threads[i] = new Thread(M i X ~ m k [ -() ->{ @ A q ! s Z 6; {
                { # * $for (int&nb` ? b 2 & Vsp;j = 0; j < 1000; j++) {
&n, y , 5 D J tbsp;       &nbx J b ] 3 1 6 Bsp;    &7 P A Z X knbsp;     &nbsc N Gp;atomicInteger.incrementAndGet();
 &nb8 ( h U _ / t ssp; &nK o d t P *bsp;     &nbsB j e 8p;  &K x j onbsp;   }
  d ~ 3 V 6;          });
           &n| K I 4 J U e ? kbsp;threads[i].starm K * D 7 e Wt();
        }
 &n+ # Rbsp;  &n. O Q o D ;bsp; &nbsP F R rp; join(threads);
   &_ Q p 3 nnbspe D l k @ u L;    System.out.println(9 ` , ? l $\"xJ ) 0 d 8 i=\" + atomicInteger.get());
&nbs- J 8 gp;   }

    private static void join(Thread[] threads) {
    &nb T p ~ } .bsp;   for Z = 2 ! 1(int G g c ) .ie . y =M i S u B 0; i < 20; i++) {
            N $ : $ /;try {
            &nbsS N _p;   threads[i].join();
  } E x & { l :;  &u & &nbsp;       }x P b m y c - catch (6 Y * | *InterruptedException e) {
       * m Y;&nbs$ a . Vp;&nbsg k 8 ] ] b @p;   & p g 6 xnbsp;   e.printStackTrace();
    T Q V u _ 4 { d J        }
&nbs@ R g d Up;       }
    }
}

运行结果:0 x 2 ; c c !

CAS、原子操作类的应用与浅析及Java8对其的优化

这就是原子C | 2 | B ) J %操作类的神奇之处了,在高并发的情况下,这种方法会比Synchronized更有优势,毕竟Synchronized关键字会让代码串行化,失去了多线程优A ; |势。

我们再来看个案例:

如果有一个需求,一c g j T v o i个字段的初始值为0,开三个线程I | l 7 @ O 2

  • 一个线程执行:当x=0,x修} g ; f改为^ & | ;100
  • 一个线程执行:当x=100,x修改为50
  • 一个线程执行:当x=50,x修改为60
    public w m % W F n B A 0;static void main(String[] argsY m } $ z 4 # u) {
&nF % T Mbsp;       AtomicInteger atomicInteger=new AtomicInteger(* J p [ . E);
        new Thread(() -> {
           &nbs* f D z fp;if(!atomicInteger.compareAndSet(0,100)){
   &( P H @ q `nbsp;     &T 7 _ 4 r #nbsp;     &nbs, r Ep;System.out.println(@ Z c X f k i D\"0-100:失败\");
   4 | * , 4 + . H;         }
 * ] 7 8 l V f s i o x N 1 p     &nbQ q u zsp;}).start();

 P ~ P( - b x;      new Thread(() -> {
          &ny g ) S ; % : % gbsp; try {
           &nbsP ! [p; 2 d H U;   Thread.sleep(500);////注意这里睡了一会儿,目的是让第三个线程先执行判断的操作,从而_ % 3 M让第三个c / _ ~ k . F ; k线z ~ c 5 5 X * y程修改失败
    3 F v n B =;      &nb1 P 5 9 Ksp; } catch (InterrupteZ W + @ J O & x YdException e) {
       &] d X / &nbsp;&nbt 6 q % l ( Us0 : d }p;       e.printStackTra_ X )ce();
      % e : $ = : $ a &x L = lnbsp;  &x 4 ? A G Q E jnbsp; }
  &n^ y m ;bsp;  ! 9 x 7 A       if(!atj 0 7 K Q _ 0omn M sicInteger.o C L G - BcompareAndSe t w jt(100,50)){
                System.out.pB { % lrintln(\"100-50:失败\");
        # [ c k Y H 6 k;    }
   D q w 7 } ( =&n^ a l h 4bsp;   &+ z x ! enbsp;}).start();

        new Thread(() -> {
 &l [ ~ N H j @ -nbsp; &s { L onbsp;       { ~ | S e & { g if(!ato: + f 4 rmicInteger.compareAndSet(50,60)){
&~ | onbsp;    &nZ C Ubsph ) / ; R 9;          System.out.println(\"50-60:失败\");
         &nbs% @ u F Yp;/ T h D A O 1 z  }
       &nb+ } L L 8sp;}).start();

    &nb j W E lsp;z { E & Z   try {
  &n% I H w /bsp;   e r 2      Thread.sleep(1000);
      6 ; t X b y;  } catch&w T O / l @nbsp;(InterruptedException e)&nb4 R l c * P j O Gsp; 1 7{
&nbm # + I I ` ~ Asp;           e.printStackTrace();
      &8 0 y : ] O v 2nbsp; }
    }

运行结果也是一样的:

CAS、原子操作类的应用与浅析及Java8对其的优化

这个例子好像没有什么意思啊,甚至有点无聊,为什么要举这个例子呢,因为在这里,我所调用的方法compareAndSet,首字母就是CAS,而且传递了两个参数,这两个参数是在原生CAS操作中必l i } R须要R J * ; * q 传递的,离原生的CAS操作更近一些。

既然原k = I ] p ; f子操作类那么牛逼,我们很有必要探究Z 7 V f & I v y下原子操作类的基石:CAS。

CAS

CAS的全称是Compare And Swa8 a a F D 2p,即比较交换,当然还有一种说法:Compare And Set,调用原生CAS操作需要确定三个值:

  • 要更新的字段
  • 预期值
  • 新值

其中,要更新的字段(变量)I P T {有时候会被拆分成两个参数:1.实例 2.偏移地址。

也许你看到这里,会觉得云里雾里,不知道我在说什么,没关系,继续硬着头皮看下去。

我们先来看看compareA] w &ndSet的源码。

compareAndSet源码浅析

首先,] @ O y - u调用这个方法需要传递两个参数,一个是预期值,一个是新值,这个预期值就是旧值(是值,不是字段),新值就是我们希望修改的值(是值,不是字段)。我们来看看这个方法的内部实现:

 public final boolean&np C N = fbsp;comparb 0 ) - feAndSet(int expect, int update) {
     & 8 e ~ ; mnbsp;  return unsafC ( y T & o I @ Ze.compaO k ! ?reAndSwapInt(this,&nb~ _ +sp;valueOffset, expect, update);
    }m S u j

调用了unsafen 1 : m z 4 z X I下的compareAndSwapInt方法,除了M # ] 6 ! ,传递了我们传到此方法的两个参数之外,又传递y + k了两个, O S @ / n | h u参数,这两个参数就是我上面说的实例和偏移地址,this代表是当前类的实例,即AtomicInteger类的实例,这个偏移地址又是什么鬼呢,说的简单点,就是确定我们需要修改的字段l X y 1 B 4 p 7 ]在实例的哪个位置。

知道了实例,知道了我们的需要修改的字段是在实例的哪个位置,就可以确定这个字段了。不过,这个确定的过程不是在Java中做的,而是在更底层做的。

偏移地址是在本类的静态T I w代码块中获得h ( m 1 A y ~的:

    private sti u d |atic&nbg ; 1 | D @sp;final long&nV n R ibsp;valueOffset;

 &nbst t v $ s 4 gpl ` i 5;  st` ~ g Q 6 @ * (atic {
&nk ? Pbsp;T _ M   &nbsJ L E Ip;&nb? 3 zsp;  try {
    J g    &nbS B n G b g K Asp;   [ ; SvalueOffset = unsafe.objectFielda f g N x { z 9 |Offs5 k e Wet
    &nU b 6 X x 9 3 zbsp; &nF z % T 1 7 F & hbsp;       &nbs_ C # * S Hp; (AtomicInteger.class.getDeclaredField(\"value\"));
        } catch (Exception ex) { throw new Error(ex); }
    }

unsd ( 4 * Tafe.objectFielda U 2Offset接收的是Field类7 ] h 4型的参数,得到的就是对应字段的偏移地址了,这里就是获得valL - u o @ue字段在本类,即AtomicInteger中的偏移地址。

我们在来看看value字段的定义:

 pa k , ^rivate volatile int valu ? ] .e;

volatile是为了保证内存的可见性。

大家肯定想一探究竟compareAnd& ! * 3 ) x )SwapInt和objectFieldOffset这两个方法中做了什么事情,很遗憾,个人水平有限,目前还没有能力去探究,只知道这种写法是JNI,会调用到C或者C++,最终会把对应的指令发送给CPU,这是可以保证原子性的。

x m i 4 % | { ? 们可以看下这两个方法的定义:

pu# W # g y -blic final native bool` C H | [ 2 q # kean compareAndSwapInt(Object var1, long var2,&! ` f onbsp;int var4, int / q 3 @ # 3 N a (;var5);
public&nb} K 2 s u $sp;native long objectFieldOffset(Field var1);

这两个方法被nativc + z x _ / !e标记了。

我们来L u k R为compareAndSwap! 5 w h ` N a m ?Int方法做一个比较形象的解释:

当我们执行compareAndSwapInt方法,传入10和z V D j ] @ r B e100,Java会和更底层进行通信:老铁,我给你了字段的所属实例和偏移地址,你帮我看下这个字段的值是不是10,如果是10的话,你就改成100,并且返回true,如果不是的话,不用修改,返回false把。

其中比较的过程就是compare,修改的值的过程就是swap,因为是把旧值替换成新值,所以我们把这0 } * 0 N r _ x O样的操作称为CAS。

我们再来看看incrementAndGet的源码。

incrementAnF | n 5 3 ` 6dGet源码浅析

    public final int incrementAndGet() {
     &nb/ z ( Q ` L $ Ksp; w ( b return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
&nbm $ 2 E Esp;   }

    public final # ! m jint getAndAF * K |ddInt(Object var1, long var2, int var4) {
    , / W M ; u ~ I :    int&nbP M nsp;var5;
        do {
 &nbs7 3 f |p;          var5 = this.getIntVolatile(var12 J #,&c P I /nbsp;var2);
        } while(!this.compareAndSwapInt(var1,&nbs@ v z w O up;var2, var5, var5 + var4));

        return var5;
 &p q nnbsp;  }

incrementAndGet方法会调到用getAndAddInt方法,这里有三个参数:

  • var1:实例。
  • var2:偏移地址。
  • var4:需要自增的值,这里是Z l U S Z M h | M1。

getAndAddInt方法内部有一个while循环,循环体内部根据实例和偏移地址获得对应的值,这里先称为A,再来- C o看看while里面的I : L s i b u v m判断内容,JDK和S O B - ! ; O更底层进行通讯:嘿,我把实X 0 T C ; z G例和偏移地址给你,你帮我看下这个值是不是A,如) f 5果是的话,帮我修改成A+1,返回true,如果不是的话,返回false吧。

这里要思考一个问题:为什么需要while循环?

比如同B { j d L 8 X V时有两个线程执行到了getIntVolatile方法,拿到的值都是10,其中线程A执行native方法,修改成功,但是线程B就修改失败了啊,因c ` - : D z为CAS操作是可以保证原子性的,所以线程B只能苦逼的再一次循环,这一次拿到的值是11,又去执行native方法,修改成功。

像这样的N A A N Mwhile循环,有一个高大上的M Y E称呼:CAS自旋。

让我们试想一下,如果现在并发真的很高很高,会出现什h ( c h M ; R T么事情?大量的线程在进行CAS自旋,这太浪费CPU了吧。所以在Java8之后,对原子操作类进行了一定的优化,这个我们后面再说。

可能大家对于原子操作类的底层实现,还是比较m r E ; B 4 & n迷茫,e ` T 5 1 p 7 z还是? a W T m不知道unsafe下面的方法到底是什么意思,毕竟刚才只是简单的读了下代码,俗话说“纸上得来终觉浅,绝知此事要躬行”,所以我们需要自己调用下unsafe下面的方法,来加深理解。

Unsafe

Unsafe:不安全的,既然有这样的命名,说明g & w J @ P #这个类是比较危险的,Java官方也不推荐我们直接操作Unsafe类,但是毕竟现在A ` k是学习阶段Z K H y,写写demo而已,只要不是发布到生产环境,又有什么关系呢?

Unsafe2 Y H C D u l O下面的方法还是比较多的,我们选择几个方法来看下,最终我们会利用这几个方法来完成一个demo。

  • objectFieldOffset:接收一个Field类型的数据,返回偏移地址。
  • compareAndSwapInt:比较交换,接收四个参数:实b p 2 = -例,偏移地Z 6 -址,预期值,新值。
  • getIntVolatile:获得值,支持Volatile,接收两个参数:实例,偏移地址。

这三个方法在上面的源码浅析中,已经出现过_ y , j [ T R了,也进行了一定的解释,这里再解释一下,就是为了加深印象,我在学习CAS的时候,也是反复的看博客,看源码,突然恍然大悟。我们需要用这三个方法来完成一个demo:写一个原子操作自增的方法,自增的值可以自定义,没错,这个方法上面我已经分析过了。下面直接放出代码:

public class MyAtomicInteger {

    priO & 3 ` O vvate volatile int value;

  &nbs_ q ) K D I 6 Q _p; private static long offset;//偏移地址

 &r d wnbsp;  private static U k L @Unsafe unsafe;

    static {
        try {
      &n, W T e C 2 lbsp;     Field theUnsafeField = 3 6 e = } F # UUnsafe.class.getDeclaredFs ! { ; P ) 2ield(\"theUnsafe\");
    &nbs/ X g 8 @ g &p;   / 1 m & : H S ^;    theUnsafeField.setAccessible(true);
           c } d _ . @ unsafe = (Unsafe) theUnsafeField.get(null);
     &nL f ] @ m ) - Jbsp;   &ne 7 t ? %bsp;  Field field = MyAtomicInteger.c| s = B ~ XlassG ] ^ ~ s w.getDeclaredField(\"value\");
       / t x 5 ! u l x C;   &nbi 6 X , % / ]sp; offset = unsafe.objectFieldOffset(field);//获得偏移地址r % ! ;
      ` k E  } catch (Exception e) {
          &nb2 X I Nsp; e.printStackTrace();
      &nbsa } 0 o c - ip; }
  B : h - _ M &nbs! V Z ^ 4 cp;}

    public void increment(int T Z _ + K $ ] :;num) {
  x $ T J;      int tempValue;
        do {
      &nbX v Msp;   E } C | g S 5 L;  tempValue = unsafe.getIntL - W j # 6 XVolatile(this, offset);//拿到值
        } while m 9 K { f ! #;(!unsafe.compareAndSwapInt(this, offset, tempValuN 0 l q V + Ve, g + P &value + num));//CAS自旋
    }

   ! ? A; public&ni J k 5 m , + h !bsp;inK X xt get() {
        return&nbsR * & vp;value;
  x e P 1 H x;  }
}

public class Main {
 &Q m v W I y R &nbsp; &n5 U } e 7 G d yb% s @ 7 L bsp;public static void main(String[] args) {
 &E L Z ? R l L b znbsp;  / B @;    j % % 4 ! _ ) s;Thread[] threads =K ! M % new Thread[20];
        MyAto@ D 0 i =micInteger atomicInteger = new MyAtomicInteger();
       &3 m 5 9 2 U &nbsp;for (int i = 0; 4 I W 8 Q . ( O;i < 20; i++)&n5 * m { u Ebsp;{
        &: @ l +n3 E @ O H Lbsp;   threads[i] =&nbss | u 2 u p y Np;new Thread(() -> {
   &nbsU 9 k jp;            for (int j = 0; j < 1000; j++) {
    ~ r i t 2 [ e &nbs@ E ~ ^ Z z & .p;   &* N f G D $ | =nbsp;  &nbV l csp;    &nbN 3 ? U 0 / Bsp;  atomicInteger.increment(1);
                }
    &nbsk ^ d b R % %p; &nbE { % 0 8sp;   &nbsd y *p; });
           &nA = G e M lbsp4 7 + 9 A;t[ P n Nhreads[i].start();
      z + g L u A }  }
  n D t; &g Q M 2 _ Dnbs# T _ 3p;  d @ x;  for (int 6 % M =i = 0; i < threads.length; i++) {
   &nb7 B 5 H S j l R jsp;    ( x i W z %    try&nb& ] d j 4 _sp;{
          &nbs 8 O . N P Zsp;   &nbJ q Y psp; threads[i].join();
    &nbc 2 ?sp;       } catch (InterruptedException e) {
                N 4 t g A . s @;e.printStackTrace();
            }
        }
        System.u 0 wout.println(\"x=\" + atomicInteger.get());
    }
}

运行结果:

CAS、原子操作类的应用与浅析及Java8对其的优化

你可能会有疑问,为什么需要用反射来获取theUnsafe,其实这是JDK为了保护我们,让我们无法方便的获得unsafe,如果我们和JDK一样来获得unsafe会报错:

    @C8 # r v L p `allerSensitive
&nb; C 9 Z 7 I t 5sp;   public static Unsafe ge; $ 2 5 K h w u ~tUnsafe() {
        W u v | h ` w UClas8 | w t Ks&nbg 9 / -sp;var0 = Reflection.getCalT P _ W . ~ 2 [lerClass();
        g z ^ Y ^ e : +if (!VM.isSystemDomainLoader(var0.getClassLoader())) ? | J x R V p {
     &n7 a G 8 K I h o @bsp;      throw new SecurityException(\"Unsafe\");/V ^ L ;/如果我们也以getUnsafe来获得theUnsafe,会抛出异常
        } else&n | A a : inbsp;{
            return theUnsafe;
       * ] Z @ V C + ! G; }
    $ 7 t;}

CAS与单例模式

对的,你没看错,我也没写错,用CAS也可以完成单例模式,虽然在正常开发中,不会有人用CAS来完成单例模式,但是是检验是否学会CC e | P S : q .AS的一个很好的题目。

public cY c t D 8 C c X Olas| s G (s Singleton&n. G _ 5 p `bsp;{
     B , k k = g;private Singleton() {? ! ` $ 8 g j
    }

    private static AtomicReference<Singleton> singletonAtomicReference = new AtomicReferencer a D v V<>();

 X g;   public statid ] Yc Singleton getInstance()&H } r dnbsp;{
        while (true) {
 &nbsy I Ep;      A r =&nY M ~ M C [ Sbsp;   Singleton singleton = singletonAtomicReference.get();//K 2 [ g w _ ` L 获得singleton
      &nb4 L zsp;     if (singleton != null) {// 如果singletonK k m ^不为空,就返回singleton
 &nbsj P ^ 9 ip;          &, [ = $ P k ) vnbsp;  &nn @ k 1 J v 6 Dbsp;return singleton;
     &nM S W 1bsp;  &ni Xbsp;   }
 & ^ F y E;         w T L B ) D @ &;  //e + N / 如果singleton为空,创建一个singleton
  = D N ^ 6 &nbz h + * i $ G o Ksp;     &n- _ g 8bsp; N # H T } }  singleton = new SinglZ ? S RetoB ( q ^ `n();
   &nbs. , * T @ p np;        // CAS操作,预期值是NULL,新值是singleton
     n y }       // 如果成功,返回singleton
            // 如果失败,进入第二次循环,singletonAtomicReference.get()就不会为空了
&nA f gbsp; f i X B h ^ 4;     J % J V I ;     K _ H;if (singletonAtomicReference.compareAndSet(null, singleP s ] p z # 3ton)) , z t h a b y;{
 &n( : 3 `bsp;      k 0 D D # X s %     C m P H V x   return singleton;
&nbe L _ 6 y z a W ;sp;    o 2 u c b ,;       }
  &nbS u 4 Y w 3 D xsp; &F t ]nbsp;   }
  &nbp X E 0 O 3 %sp;J w z 9 ! }
}

注释写的已经比较清楚了,可以对着注释,再好好理解一下。

ABA

compareAndSm # 8 B , }et方法,上面已经写过一个de! Q 0 r C K } `mo,大家可以也试着分析a l 6 s I H {下源码,我就不再分析了,我之所以要再次提到compareAndSet方法,是为了引出一个问题。

假设有三个步; = g g R q骤:

  • 修改15E ^ + / ? w0为50
  • 修改50为150
  • # ! { / M改150为90

请仔细看,这三个步骤做的事情,一个变量刚开始是150,修改成了50,后来又被修改成了150!(又改回去了),最后如果这个变量是1b V X50,再改成b 2 y / / I90。这就是CAS中ABA的问题。

第三步,判断这个值是否是150,有两种不同的需求:

  • 没错啊,虽然这个值被修改了,但是现在被改回去了啊,所以第三步的判断是成立的。P @ = U b | -
  • 不对,这个值虽然是150,但是这个值曾Z h h [ u B经被修改过,所以第三步的判断是不成立的。

针对于第二个需求,我们可以用AtomicS- ~ 5tampedReference来解决这个问题,AtomicS/ ; ) W / ~tampedReference支持泛型,其中有一J / = A w个stamp的概念。下面直接贴出代码:

   &n5 ) + t  + _ bsp;pu2  ; Oblic stW 0 e t X ) h latic void main(String[] args) {
       t $ m A @ A D u;{ 3 8 ` try {
   &] 3 O 4 anbsp;        AtomicStaG L X ~ 5 Z LmpedRefer_ ] _ 7 y }ence<Integer&gj x / ] 7t; atomicSt9 V e DampedX ! v J d X h (Reference&n] B # $ kbsp;= new ? u (AtomicStampedReference<Integer>(150, 0);
     &8 r m ^ S unbsp;      Thread thread1 = new Thread(() -> {
    b [ v 4;   &nbP _ w 6 V P xsp;       &nu : 6bsp;Integer oldValue z u O y C h v 8 N;= atomicSv a U ? 3 s b L +tampedReference.getReference();
                int stamp = atomicStampedReference.getStamp();
       &nbsM k D I Wp;        if&nbsv m 4 ~ Sp;(atomicStamP V U t 5 ( Q ypedRM T [eference.compareAndSet(oldValue, 50, 0,&nbsZ R Z q 3p0 Z 0 N ; I;stamp + 1)) {
 &nbs4 l p c / Sp; &nbf Z Qsp; &nbsA n A L [ d w Rp; A C J b @ E z e +             System.out.println(\"150->50&nbs a h 7 V [ a Isp;成功:\" + (stamp + 1));
&nbs5 1 ( = } wp;         * i j b 6 a #      }
            });
     &nbs- x f l F !p; &nb( 0 | |sp;   &nbsN @ i U ep;Z X t d } = B |thread1., [ B ; r I z N rstarta 1 3 S L();

            Thread thread2 = new Thread(() -> {
      &nbsu } = Np;         try&Y * i x n J @ ^ 3nbsp;{
    &2 # d J M { # ynbsp;               Thread.sleep(1000);//睡一会儿,是为了保证线程1 执行完毕
 9 ~ K |;      E l f 6 y c &5 x , { 9 - s :nbsp; &nX j # ?bsp;     } catch (Interru* B V NptedException u 5 ; n u;e) {
             &w 7 rnbsp;  &n^ P F 6 0 y k Y .bsp;   e.printStackTrace();
           &nz K @ 4 6 $ p 0bsp;    }
          &nb~ I 9 7 ~ g Usp;    &1 j ; F W Pnbsp;Integer oldValue = atomicStampedReference.getReference()w $ m _ B L 9 (;
          &nbs+ n R 9 :p;     int stamp = atomicStampedReference.getStamp();5 L ` W 2 f $ R a
   &8 6 L P x P 3 x @nbsp; &nH | q { w Xbsp;          if (atomicStampedReference.compareAndSetM * | p i ](oldValue, 150, stamp,&B v { l 5 3 ` T Pnbsp;stamp + 1)) {
             &P u q M - Jnbsp;     &nby ) h Y wsph A ` 7 N x;System.out.println(\"50->150 成功:\" + (stamp + 1));
                }
            ) = f {});
     k ; N;&nB F { Jbsp;      thread2.start();

           c 0 * g l j p + Thread thread3 = new Thread(() h , 7 P S |;-> {
 &nbs- K ! 6 1 @ # Up;         H J W g;   M Y ^;  try {
&nbx #sp;&F 8 H 1 - h ` ynbsp;          &nbn F # v m esp& @ J;  &l B C d A Dnbsp;    Thread.sleep(2000)( { $ Q =;//睡一会儿,是为了保证线程1,线程2 执行完毕
        M M Y D ] 2 N ?;        [ G ; ] 4 X e l;} catch (InterruptedException e)A 4 s m L i D 1 {
   &nbQ 6 . 1sp; &nc x l 6 m ^ X 0bsp;     &6 v s @ s p 2 {nbsp;   b M P ? & T H;      e.printStackTracW f `e();
       &nb~ c i m y R 8 %sp;     ~ 0 { | o;   }
 &nbs) ? %p;           &n$ ] ?bsp;  Integer J y P 2 q m J [ T;oldp U * D = TValue l O D ~ V ] { k e;=&f 1 Onbn W e 6 5 Vsp;atomicStampedReference.getReference();
                int stamp&nbs. 3 )p;= atomicStampedReference.getStamp();
  &n % S ^ m 9 p nbsp;             if (atomicStampedRO | j : u ? ,eference.compareAndSet(oY / F U Y p , }ldVa4 ? C 1 T u @lue, 90, 0, stamG 4 ip&nb; 8 c U R % ^ O 5sp;+ 1)) {
            % @ r E&nN : 9 e 9bsp;       System.out.println(\"150->90 成功:\" + (stamp + 1));
   &nbs0 b _ W R Dp;      &M L ( s [ G 9 Dnbsp;     }
  } F | Z `;  &nU # b gbsp;    &nbs0 Z V / 5 Vp;  });
            thd A I X Bread3.start();

&% , o ( , F o {nbsp;       &H 6 Fnbsp;   thread1.joil 8 A %n();
 &nbsg t W - ; Jp;      &] ` e _nbsp;   thread2.join();
 1 C L c b Q;           tm K T bhread3.join();
  &n} + S /bsp;         System.out.println(\"现在的值是\" + atomicStampedReference.getReference() + \";stamp是\" + atomicStampedReference.getStamp());y ] C z 3 X V
   h ^ B $ 7 G     } catch (Exception e) {
   k h v H b n;  r d % | y W;  &q ^ S 4nbsp;    e.printStackTrace();
&U c l ( q - Pnbsp;    &, y + /nbsp;  }
    }

Java8对于原子操作类X D a k的优化

在进行incrementAnd{ V u { N m 3 }Get源码解析的时候,说到一个问题:在高并发之下,N多线程进行自a P y旋竞争同一个字段,这无疑会给CPU造成一定的压力,所以在Java8中,提供了更完善的原子操作类:Lo9 ~ k vngAdder。

我们R i @ @ H { .简单的说下它做了下什么优化,它内部维] a :护了一个数组Cell[]和base,Cell里面维护了G T qvalue,在出现竞争的时候,JDK会根据算法,选择一个Cell,对其中的value进行操作,如果还是出现竞争,会换一个Cell再次尝试,最终把Cell[]里面的value和base相加,得到最终的结果。

因为其中的代码比较复杂,我就选择几个比较重要的问题,带着问题去看源码:

  • Cell[]是何时被初始化p ! 6 ; a的。
  • 如果没有} w O Y a竞争, 9 v ^ n _,只会对base进行操作,这是从哪里看出来的。
  • J k . C N Q始化Cell[]的规则是什么。
  • Cell[]扩容的时机是什么R y S # w b K f S
  • 初始化Cell[]和扩容Cell[]是如何保证线程安全性的。

这是LongAdder类的UML图:

CAS、原子操作类的应用与浅析及Java8对其的优化

add方法:

 public void add(long x) {
        Cell[] cs; long b, v; int m; Cell T 8 f 7 i Fc;
        if ((cs = cells) != null || !casBase(b = base, b + x@ G ; 1)) {//第一行
&d h Ynbsp;        &nbsF V J O E @p;&V e z : Unbsp; booleaI D ! I q #n uncontended = true;
   &nbC z Gsp;     0 w 2 m   if (cs =_ M y= null || (m = cs.length 1 N K h i E r;- 1) < 0 ||//第二行z ~ @ z W J
                (c = cs[getProbe() &&? 7 T ynbsp;m]) == null ||//第三行
&q . N ~nbsp;          &p + 9 qnbsp;  0 T v V 3 6 5;& ] i Hnbsp; !(uncontended = c.cas(v =&nbsC v n l ? gp;c.value, v + x)))//第四行
             &t / 4 ~ U B Onbsp;  long0 0 / d + T c } fAccu% R = #mulate; R o ) * x 5 &(x, null, uncob ` W = { ; q zntended);//第五行
        }
    }

第一行:||判断J u @ % & | { J w,前者是判断cs=cells是否【不为空】[ 5 C 2 u X 9 o,后者是判断CAS是否【不成功】 。casBase9 s 4 w R B h X做什么了?

final booR H ; z Alean casBase(lo- G 6 z  n 3 bng cmp, long val) {
  &nbsY I Y &p;     return BASE.compareAndSet(this, cmp, val);
}

这个比较简单# } w p c D S,就是调4 ; c x用compareAndSet方法,判断是否成功:

  • 如果当前没有竞争,返回t* e { l u Yrue。
  • 如果当前有竞争,有线程会返回false。

再回到第一行,整体解释下这个判断:如S P T d - ! #果cell[]已经被初始化了,或者有竞争9 ? | 6 N,才会进入到第二行代码。如果没有竞争,也没有初始化,就不会进入到第二行代码。

这就回答了第二个问题:X ` b e 7 m * `如果没有0 n N h Q y ~ J竞争,只会对basel Z R i &进行操作,是从这里看出来的。

第二行代码:||判断,前者判断cs是否【为NULL】,后者判断(cs的长度-1)是否【大于0】。这两个判断,应该都是判断Cell[]是否初始化的。如果没有初始化,会进入第五行代码。

第三行代2 , Q : - K $码:如果cell进行了初始化,通过【getProbe5 I ` s { I() & m】算法得到一个数字,判断cs[数字]是否【为NULL】,并且把cs[数字]赋值给^ o ^ I了c,如果【为NULL】,会进入第五行代码。

M M 7们需要简单的看下getProbe() 中做了什么:

    static final int getProbe() {
  &nbD 7 @ 0 %sp;  ) m ] r = c s P; &` @ L P n E Lnbsp; return (int) THREAD_PROBE.get(Thread.currentThread());
    }

    private static final VarH) P a [ V handle THREAD_PROBE;

我们只要知道这个算法是根据THREAD_PROBE算出来的即可。

第四行代码:对c进行了CAS操作,看是否成功,并且把返回值赋值给uncontended,如果当前没有竞争,就会成功,如果当前有竞争,就会失败,在外面有一个!(),所以CAS失败了P 2 n,会进入第五行代码。需要注意的是,这里已经是对Cell元素进行操作了。

第五行代码:这方法内部非常复杂,我们先看下方法的H 4 z N 2 7整体:

CAS、原子操作类的应用与浅析及Java8对其的优化

有三个i5 j - O ,f:

1.判断cells是否被初始化了,如果被初始化了,进入这个if。

这里面又包含了6个if,真可怕,但是在这里,我们不用全部关注,因为我们的目标是解决上面提出来的问题= 5 @ A

我们/ 3还是先整体看下:

CAS、原子操作类的应用与浅析及Java8对其的优化

第一个判断:根据算法,拿出cs[]中的一个元素,并且赋值给c,然后判断是否【为NULL】,如果【为NULL】,进入这个if。

          &nbsp l * 9 } H A Mp;  6 B 2 3 8       if (cellsBusy == 0) {   &o M e x Qnbspv : 3 D * % } S;   // 如果cellsBusy==0,代表现在“不忙”,z l v O B W Y进入这个if
                 n - } f ? b d T ;    &@ 1 % 2 a g d 2 gnbsp;  Cell r =&nbsq c + Y y | H bp;new Cell(x);   //创建一个CelA 5 q 6 R I $ z Xl
 &nbsv u ^ 3 M K X m ,p;     &nbsM d . ~p;    &nbsa h & hp;           if (cellsBusy&nbs[ m } g V 3 R T Qp;== 0 &&aS _ N qmp; casCellsBusy()) {//再次判断cellsBu- , ssy ==0,加锁,这样只有一个线程可以进C C m m C k m c入这个if
 &n] = obsp;             &nn H @ R ` Y absp;    &6 4 Z D E ,nbsp;      &nb[ i 6sp;//把创建出来Cell元素加入到Cell[]
     O = w S l H ! h;&ng q [ R [ + cbsp; ) 3 V B V 5 1 h  &nbs( e # 8 q m Bp;                  d ! 7 m Q W l A Qtry {  &a 1 : 8 B K y $nbsp;    
 u J f;            &Z [ C 1nbsp;                  Cell[] rs; int&nbh i & F , 3 v Osp;m,f 8 M j;
            &nbY ; ( g psp;               &nbg f q | 2 F * ~sp;   if ((r& + d ( ] = ` W es = cells) != null &&
        &nN w F o v k ^ 8bsp;    &nbsH o :p;    &nbsm i a T 7 %p;                &nP * 2 Mbsp;(m = rs.length) > 0 &&
    &nbS I usp;  &nbsi # U j ` ~ ~p; 0 = K | | P L &nbq m osp;             v - j 3 : ` @ & y   K I m X ! K J  C ) o ! #     U d ~ } E  rs[j = (m - 1) & h] == null)8 = y 7 ) y J {
    &m B h G ? { 4 v 9nbsp;    c o z p } r . 9 F;                     ? , b f ? ! / ) 3      rs[j] =&nbsT t _ W b : 5p;r;
  | O F L ! Z;     * r R ;              

上一篇

七位男星大合集 肖战妖娆帅 李现英俊 谁才是你心目中那个男神?

下一篇

作为老人,诸葛亮一天能吃三四升米,司马懿怎么能判断他活不长?

你也可能喜欢

  • 暂无相关文章!

发表评论

您的电子邮件地址不会被公开。 必填项已用 * 标注

提示:点击验证后方可评论!

插入图片
返回顶部