今天我们的内容是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 & }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 !
这就是原子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 / &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; }
}
运行结果也是一样的:
这个例子好像没有什么意思啊,甚至有点无聊,为什么要举这个例子呢,因为在这里,我所调用的方法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 &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 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());
}
}
运行结果:
你可能会有疑问,为什么需要用反射来获取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图:
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整体:
有三个i5 j - O ,f:
1.判断cells是否被初始化了,如果被初始化了,进入这个if。
这里面又包含了6个if,真可怕,但是在这里,我们不用全部关注,因为我们的目标是解决上面提出来的问题= 5 @ A。
我们/ 3还是先整体看下:
第一个判断:根据算法,拿出cs[]中的一个元素,并且赋值给c,然后判断是否【为NULL】,如果【为NULL】,进入这个if。
  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 ;
本文系本站编辑转载,文章版权归原作者所有,内容为作者个人观点,转载目的在于传递更多信息,并不代表本站赞同其观点和对其真实性负责。如涉及作品内容、版权和其它问题,请与本站联系,本站将在第一时间删除内容!