CAS概念和解析
一、CAS概念
package syncbasics; import java.util.concurrent.CountDownLatch; /** * 多线程访问同一份数据,会产生竞争,race condition => 竞争条件 * 就有可能产生数据的不一致,并发访问之下产生的不期望出现的结果 * 如何保障数据的一致呢?---->线程同步(线程执行的顺序安排好), * 具体:保障操作的原子性(Atomicity) * 1.悲观的认为这个操作会被别的线程打断(悲观锁)synchronized * 2.乐观的认为这个操作不会被别的线程打断(乐观锁)cas操作 * CAS = Compare And Set/Swap/Exchange * * ++操作: * 把n从内存里面读到寄存器里,加完了之后,再写回去。 * 还没来得及写回去的时候,另外的线程读到了原值, * 因此,++这个正在执行的操作被另外线程打断了。 * * 只要我们能保证这个执行的操作不被打断,即保证我这个线程读过来之后改完这个值1,再写回去之后,其他线程才能执行, * 那最后的结果一定是对的。这种不能够被打断的操作称之为原子操作。 * * 什么样的语句是原子性的?什么样的不是? * java内存中的8大原子操作,了解即可。要查汇编手册。 * * synchronized: * 让原来的并发变成了序列化。 * synchronized本身是保证可见性的,n++结束了之后这个线程一定是要和主内存做同步,主内存里一定都是最新的。 * synchronized保障了可见性、原子性。 * 那么保证有序性吗? * ----不可以,synchronized里面的代码块里面的操作指令完全有可能换顺序。DCL要加volatile就是证明了。 * * CAS概念: * 还以下面这个小程序n++操作来说明CAS的概念。 * n开始等于0,线程1把n读过来加1,原来是需要加锁的,现在整个过程不上锁了,把0读过来改成1之后,再往回写 * 的过程之中做个判断,判断原值依然是否为0,如果依然为0,说明在线程1读0加1的过程中,没有人来过,那就直接把1 * 写回去,搞定。 * 万一中间有人改了呢?万一其他线程已经将0改成8了,线程1把1往回写的时候发现原值已经变为8了,不是你所期望的0, * 这时候怎么办?那就再来一遍,把8读出来加1变成9,把9往回写的过程之中看看判断原值是否依然为8,如依然为8,说明在我 * 将8改为9的过程之中没有其他人来过,那就直接将9写回去了,搞定。 * 当然,如果将8读来加1的过程之中又有人打断了,有人将8改成100了,怎么办?那就把100读过来加1,将101往回写的过程中判断 * 原值是否依然是100....一直到某一次成功了为止。 * 你会发现它就在这里不停的循环,读取当前值,计算结果,比较当前值和新值,如果当前值和新值相等,更新为新值。如果不相等, * 就再来一遍,总有一次能成功。 * * CAS的ABA问题: * 上面的这段话描述里面有个很重要的问题,此0非彼0的问题,线程1把0读过来改成了1,往回写的过程中发现原值依然为0,但是 * 这个0是不是你所看到的那个0呢?未必,有可能在这个过程之中,这个0被别的线程改成了8,又被别的线程改回了0!中间有个 * 0->8->0的过程,此0非彼0,A->B->A,这就是ABA问题。 * 但是咱们这个程序是不存在这个问题,只是理论上有,简单数据类型就算ABA问题,但是对我而言没关系,这种可以不在乎,略过。 * 但是在有些情况下,是要解决的。如果这个值是一个引用的话,读过来引用值,对它的属性进行了一些修改,它是一个对象,当你再 * 往回写的时候,有可能这个引用指向的对象里面的内容发生了改变,引用依然还是这个引用,但是里面的内容发生了改变,这时就要 * 在乎这个ABA问题了。 * * 解决ABA问题: * 加version,你的女朋友,你早上离开了她,晚上回来发现依然是她,但此她还是彼她嘛?你比较怀疑,怎么办?在她脑门上写1.0,你就 * 走了,回来的时候依然是她,不过如果中间经过任何其他线程操作,这个ver1.0都会加1。这时候你发现她脑门上是99.0,那肯定中间经过 * 了改变,当然就看你在乎不在乎。所以加version,加版本就可以解决。 * * CAS的底层原子性保障: * 除了ABA问题,CAS还有一个巨大问题。分析一下: * 线程1将0读过来改成1,把1往回写的过程中是一个CAS操作。CAS叫compare and swap,compare and set,伪代码就是: * if(v==0){v=1},这个操作实际上底层就是两步:比较和设定。那万一当你执行完if(v==0)后,这个时间点上被另外线程打断, * 另外线程把0改成8了,那线程1又将8->1,那还是出问题了,数据还是不一致! * 所以,如果想让CAS产生作用的话,必须保证CAS操作本身必须是原子性的。 * * 悲观锁和乐观锁的效率: * 不要认为悲观锁的效率一定比乐观锁的效率高,CAS一定比上来就上锁的效率高,不是的,不一定。 * 悲观锁一般采用什么样的实现呢,一把锁lock,和这把锁关联的队列,这个队列用来等待着这把锁,比如说小明wc蹲着,这把 * 锁是他的,队列里还有小红、小花、小光、小刚...他们在等着。在队列里排着队,操作系统OS说小红轮到你了,你出来,可以抢 * 这把锁了,凡是在队列里等待的这些线程,是不消耗CPU资源的。可是与此形成鲜明对比的是,如果这时候小明在这我们用的是乐观锁, * 线程会while循环不断看小明有没有出来,很多人,小红、小花...这些人不会坐在那里安安静静的等待,他们会领着裤子原地打转。 * 这些线程消耗cpu,都是活着的线程,cpu一个是一直要运行他们的while循环,一个是要进行他们的线程切换。而等待队列里的cpu是不占用 * cpu的,他们状态是parking、waiting或blocked,什么时候OS说轮到你了你再占用cpu资源。所以乐观锁是要消耗cpu的,消耗的比悲观锁要 * 多一些。 * 使用场景: * 悲观锁:临界区执行时间比较长,等的人很多 * 乐观锁:时间短,等的人少 * 还是没有量化啊,实际中该使用悲观锁还是乐观锁呢?压测,可以写两种比较一下时间,实际中决定。 * 但是实战中啊,就用synchronized,因为synchronized现在做了一系列的优化,它内部既有自旋锁,又有偏向锁,又有重量级锁进行 * 自适应的升级过程,自动完成锁升级,它的效率已经调试的很不错了。 * */ public class T00_IPlusPlus { private static long n = 0L; public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[100]; CountDownLatch latch = new CountDownLatch(threads.length); for(int i=0; i<threads.length; i++){ threads[i] = new Thread(() -> { for(int j=0; j<10000; j++){ synchronized (T00_IPlusPlus.class) { n++; } } latch.countDown(); }); } for(Thread t : threads){ t.start(); } latch.await(); System.out.println(n); } }
二、AtomicXXX类:
package atomicxxx; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * 这里就是一种CAS机制 * incrementAndGet每次往回写的时候都要比较一下,看下是否原值是我期望的那个值。 * incrementAndGet操作自带原子性: * unsafe.getAndAddInt->this.compareAndSwapInt->native boolean compareAndSwapInt * 得益于CPU在汇编级别上支持指令:cmpxchg,但是cmpxchg不是原子性的,最终实现: * lock cmpxchg * 所以你会发现,CAS在宏观上我们叫做自旋锁,乐观锁,但它在底层上的实现,微观上的实现实际上是一个悲观锁。 */ public class T01_AtomicInteger { AtomicInteger count = new AtomicInteger(0); void m(){ for(int i=0; i<10000; i++){ count.incrementAndGet(); } } public static void main(String[] args) { T01_AtomicInteger t = new T01_AtomicInteger(); List<Thread> threads = new ArrayList<>(); for(int i=0; i<100; i++){ threads.add(new Thread(t::m, "thread-" + i)); } threads.forEach(o -> o.start()); threads.forEach(o -> { try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } }
---