概念compareandswap,一种解决多线程并行情况下使用锁带来的性能损失的机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和new值(乙)。如果内存位置的值与预期的原始值匹配,处理器会自动用新值更新该位置的值。否则,处理器什么也不做。在任何一种情况下,它都会返回CAS指令之前该位置的值。CAS实际上是说“我认为位置V应该包含值A;如果是,将B放在这个位置;否则,不要改变那个位置,只要告诉我这个位置现在有什么值。”简单的说就是在修改前做一个比较,检查数据是否被其他线程修改过,如果修改过,则取出内存中的新值与内存中的比较,直到相等,然后进行修改。如果我们要累加变量:num,num的初始值=0。1.cpu去内存中取出num;2、判断内存中的num是否被修改;3、做+1操作;4.将修改后的值写入内存中;这时候,可能会有疑惑。判断、自加、回写内存不会造成线程安全问题吗?既然cas可以解决并发编程中的安全问题,那么这个问题肯定不会发生。为什么?因为判断、自加、回写内存都是硬件保证的原子操作。硬件如何保证原子性?请先看下面的例子需求:用三个线程对一个成员变量累加10W,打印累加结果。我们使用两种方法来完成这个需求。1.普通积累(既不是锁也不是原子类)。1.使用同步。2.使用原子类(Atomic)。实现1.正常积累(既不加锁也不使用原子类)。这种方式没什么好说的,直接去代码包com.ymy.test;publicclassCASTest{privatestaticlongcount=0;/***累计10w*/privatestaticvoidadd(){for(inti=0;i<100000;++i){count+=1;}}publicstaticvoidmain(String[]args)throwsInterruptedException{//开启三个线程t1t2t3Threadt1=newThread(()->{add();});Threadt2=newThread(()->{add();});Threadt3=newThread(()->{add();});longstarTime=System.currentTimeMillis();//启动三个线程t1.start();t2.start();t3.start();//让线程同步t1.join();t2.join();t3.join();longendTime=System.currentTimeMillis();System.out.println("累计完成,计数:"+count);System.out.println("耗时:"+(endTime-starTime)+"ms");}}执行结果很明显,三个线程累加,由于cpu缓存的存在,结果远小于30w,这也是我们的预期,所以会出现后两种方案.2、使用synchronized时,使用synchronized需要注意的地方。需求要求我们三个线程分别累加10W,所以synchronized锁的内容很重要。要么直接给类加锁,要么三个线程使用同一个锁。synchronized和locking介绍内容可以参考:java并发编程中的第一种synchronized是直接给类加锁。这里我使用了锁定静态方法。让我们更改代码并在add方法中添加synchronized关键字。由于add方法已经是一个静态方法,所以现在整个CASTest类都被锁定了。/***累计10w*/privatestaticsynchronizedvoidadd(){for(inti=0;i<100000;++i){count+=1;}}第一次运行结果:第二次:第三次:这里有意思了,带锁的运行时间居然比不带锁的运行时间少?你不觉得有点不可思议吗?其实这也不难理解。这里会涉及到cpu缓存以及缓存和内存的回写机制。感兴趣的朋友可以自行百度,今天的重点不在这里。方法二:三个线程使用同一个锁修改代码,去掉add方法的synchronized关键字,在add方法中写synchronized,新建一个key(成员变量:lock),让三个累加操作使用this关键,代码如下:packagecom.ymy.test;publicclassCASTest{privatestaticlongcount=0;privatestaticfinalStringlock="lock";/***累计10w*/privatestaticvoidadd(){synchronized(lock){for(inti=0;i<100000;++i){count+=1;}}}publicstaticvoidmain(String[]args)throwsInterruptedException{//开启三个线程t1t2t3Threadt1=newThread(()->{add();});Threadt2=newThread(()->{add();});Threadt3=newThread(()->{add();});longstarTime=System.currentTimeMillis();//启动三个线程t1.start();t2.start();t3.start();//让线程synchronizet1.join();t2.join();t3.join();longendTime=System.currentTimeMillis();System.out.println("累计完成,计数:"+count);System.out.println("Time-consuming:"+(endTime-starTime)+"ms");}}结果如下:这两种加锁方式可以保证线程安全,但是这里需要注意的是如果你在方法中加上synchronized而不加static关键字,必须保证多个线程共享这个对象,否则锁会失效。原子类有许多原子类工具。我们举例的累加操作只用到其中一个。再来看看java提供的原子工具:工具类还是挺多的。让我们根据需要解释其中的一个,我们使用:AtomicLong。AtomicLong提供了两个构造函数:value:原子操作的初始值,调用无参构造value=0;调用参数构造value=specifiedvalue其中value还是被volatile关键字修饰,volatile可以保证变量的可见性,什么叫可见性?可见性有一个很重要的规则:Happens-Before规则,意思是:前一个操作的结果对后面的操作可见。线程1对变量A的修改可以立即被其他线程看到。详情请百度。让我们看看累积要求。AtomicLong提供了一个incrementAndGet()。源码如下:/***Atomicallyincrementsbyonethecurrentvalue.**@returntheupdatedvalue*/publicfinallongincrementAndGet(){returnunsafe.getAndAddLong(this,valueOffset,1L)+1L;}Atomicallyincrementsbyonethecurrentvalue.**@returntheupdatedvalue*/publicfinallongincrementAndGet(){returnunsafe.getAndAddLong(this,valueOffset,1L)+1L;}Atomicallyincrementsbyonethecurrentvalue:Atomicallyincrementsthecurrentvalue.好了,现在我们尝试将mutex修改为atomictool,改造代码:1.实例化一个Long类型的atomictool;2、在for循环中使用incrementAndGet()方法进行累加操作。修改代码:packagecom.ymy.test;importjava.util.concurrent.atomic.AtomicLong;publicclassCASTest{//privatestaticlongcount=0;////privatestaticfinalStringlock="lock";privatestaticAtomicLongatomicLong=newAtomicLong();/***累计10w*/privatestaticvoidadd(){for(inti=0;i<100000;++i){atomicLong.incrementAndGet();}}publicstaticvoidmain(String[]args)throwsInterruptedException{//开启三个线程t1t2t3Threadt1=newThread(()->{add();});Threadt2=newThread(()->{add();});Threadt3=newThread(()->{add();});longstarTime=System.currentTimeMillis();//启动三个线程t1.start();t2.start();t3.start();//让线程同步t1.join();t2.join();t3.join();longendTime=System.currentTimeMillis();//System.out.println("累加完成,计数:"+count);System.out.println("累加完成,计数:"+atomicLong);System.out.println("消耗时间:"+(endTime-starTime)+"ms");}}结果:累计结果也是:30w,但时间比mutex长。为什么?下面我们一起来剖析一下源码。AtomicLongincrementAndGet()源码分析/***Atomicallyincrementsbyonethecurrentvalue.**@returntheupdatedvalue*/publicfinallongincrementAndGet(){returnunsafe.getAndAddLong(this,valueOffset,1L)+1L;}publicfinallonggetAndAddLong(Objectvar1,longvar2,longvar4){longvar6;do{var6=this.getLongVolatile(var1,var2);}while(!this.compareAndSwapLong(var1,var2,var6,var6+var4));returnvar6;}我们看一下getAndAddLong方法,发现一个dowhile循环内部使用。看循环的条件是什么publicfinalnativebooleancompareAndSwapLong(Objectvar1,longvar2,longvar4,longvar6);这是循环条件的源码,不知道大家发现没有关键字:native,意思是java代码已经完成,这里需要调用C/C++代码,当这里调用了C++代码,这里解释一下为什么原子类的比较和赋值是线程安全的。那是因为C++代码中有一个锁。在C++中,在消息总线上加了一个锁来保证互斥,所以比较和赋值都是原子操作,线程安全的。Unsafe,这个类可以为原子工具类提供硬件级的原子性。虽然我们在java中使用的原子工具类都是无锁的,但是我们不需要考虑他的多线程安全问题。为什么原子类的效率不如互斥锁?好吧,现在让我们考虑一下为什么原子工具的效率不如互斥锁。很明显,没有锁,但是比锁慢。这是不是有点不合理?其实这是符合常识的。我们一起来分析一下。CAS(CompareandSwap)侧重于比较。当我们查看源代码时,我们发现有一个dowhile循环。这个循环的作用是什么?1.判断期望值与内存中是否相同2.如果不相同,则获取内存中最新的值(var6),此时期望值等于var6,使用修改后的期望值继续比较与内存中的值,直到发现期望值与内存中的值一致,+1后返回结果。这里有一个问题。不知道你有没有发现。就是这个循环问题。1、我们假设线程1首先访问内存中的num值=0;将其加载到cpu中;2、累加操作还没做完,cpu进行线程切换操作;3、线程2获得使用权,线程2也在内存中加载num=0,累加后将结果返回内存;4.线程切换回线程1,然后上面的操作需要和内存中的num进行比较,期望值=0,内存num=1,正常方向无法比较,新的将内存中得到的num=1加载到线程1所在的CPU中;5.此时再次切换线程,这次切换线程3;6.线程3从内存中加载num=1到线程3所在的CPU,然后将期望值=1与内存中的num=1进行比较,发现该值没有被修改。这时,把累加的结果写回内存;7、线程1获得使用权;8、线程1=1的期望值与内存num=2比较,发现不一样。这时候需要把内存中新的num=3加载到线程1中,在cpu中,然后比较期望值=2和内存中的num=2,发现一样,累加,把结果写回去到记忆中。这是多线程下的一种情况。我们发现线程1做了两次比较,真正的程序循环中比较的次数肯定会比我们分析的要多。mutex三个线程加起来10w,加起来只需要30万次就够了,而原子工具需要加起来30万次,循环很多次,可能上千次,也可能几十万次次,所以在内存中积累和操作互斥锁会比原子工具更高效,因为内存执行效率高会导致很多循环的比较,我们把这个循环称为:自旋。互斥锁在所有情况下都更快吗?当然不是。如果要操作的数据存储在磁盘上,或者要操作的数据量太大,原子类的性能会比互斥锁好很多。这很容易理解,就像内存中的单个线程比多个线程更高效(通常)。CASABA的ABA问题是什么?举个例子:变量a=0的初始值,a=0由线程1获取,切换到线程2,获取a=0,修改a为1写回内存,切换到线程3,获取内存中的数据a=1,将数据修改为0写回内存,切换到线程1,此时线程1发现内存中的值还是0,线程1认为内存中的a没有被修改。这时,线程1将a的值修改为1,写回内存。下面分析一下这波操作会不会有风险。从表面上看,似乎没有问题。累加或修改值都没有太大问题。我认为这个ABA没有风险。如果你这么认为,那你就错了。让我举一个例子。用户A使用网银给用户B转账,同时用户C也在给用户A转账。假设用户A的账户余额为100元,用户A想给用户B转账100元。用户C要转100元给用户A,用户A转给用户B,用户C同时转给用户A,但是因为用户A的网络不好,用户A点击没有反应,然后点击再次,此时会发送用户A向用户B转100元的两次请求。我们假设线程1:用户A第一次给用户B转100元线程2:用户A第二次给用户B转100元线程3:用户C给用户A转100元执行线程1时,得到用户A的余额=100元。此时切换到线程2,同样获取到用户A的余额=100元。线程2进行扣减操作(更新money-100wheremoney=100),100就是我们刚查出来的。扣除后,余额应为0元。切换到线程3,用户C给用户A转了100元,此时用户A的账户又变成了100元。切换到线程1,执行扣款操作(更新money-100wheremoney=100),扣款应该失败了。由于用户C给用户A转了100元,用户A的余额又变成了100元,所以线程1也扣款成功了。这很可怕吗?那么开发的时候需要注意ABA问题吗?还请分析一下应用场景。像之前提到的ABA问题,我们可以在数据库层面加上版本号(版本号累加)来解决。中间的原子类也给我们提供了一个解决方案:AtomicStampedReference,有兴趣的朋友可以研究一下。其实这个思路和版本号差不多。比较的时候,不仅要比较期望值,还要比较版本号。只有相同时才会修改。
