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);
    }
}

 

 

 

 

 

 

---

posted on 2022-03-19 12:46  有点懒惰的大青年  阅读(226)  评论(0编辑  收藏  举报