初探CAS

作者: xor_eax_eax | 来源:发表于2018-03-05 10:04 被阅读0次

现在的工作中,多线程肯定是经常挂嘴边的,实际上我们在做项目过程中,去写多线程的情况其实没多少,一般这些操作都是被封装在框架里,所以不会多线程的技术不会影响工作,但是如果多少懂一些原理会有助于理解代码.

使用锁

一说起并发,大家第一想到的肯定是锁,加锁确实能够很好的保证数据一致性,因为一段代码加锁后,只有获取了锁的线程才能执行该代码块,等到代码块执行完了之后并执行完成释放锁的操作后,其他的线程就可以去竞争这个锁,得到锁的线程可以执行该代码块,而没有竞争到锁的线程只能等待.

看一段没有锁的代码(使用java代码演示)

public class LockDemo {

    public static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        List<Thread> threads = new ArrayList<>();

        for (int i = 0; i < 100; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        doSomethings();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            threads.add(t);
            t.start();
        }

        //* 等待所有线程执行完毕
        for (Thread t : threads) {
            t.join();
        }

        System.out.println("num =" + num);
    }

    public static void doSomethings() throws InterruptedException {
        System.out.println("累加前num的值为" + num);
        num++;
        Thread.sleep(1);//加一个sleep防止编译器优化,2条语句可以被优化成num += 2;
        num++;
        System.out.println("累加后num的值为" + num);
    }
}

这段代码的意思是用100个线程取累加全局变量num,正常情况下num最后的值应该为200,但是结果却很意外,看console:

...
...
累加后num的值为191
累加后num的值为190
累加后num的值为192
累加后num的值为193
累加后num的值为194
累加后num的值为196
累加后num的值为195
累加后num的值为197
num =197

最后num并不是等于200,而是等于197,并且每次执行完成,num的值都不一样.
因为线程读取到的值并不一定最新的值,例如条线A程拿到num的时候是100,但是线程B很快就更新了num,使num变成了101,但是线程A还是拿100++,所以最后又把num设置成了101,这样有一次计算就白做了.

如何解决这个问题呢,最简单的做法就是加锁

加锁之后的代码

public static void doSomethings() throws InterruptedException {
    synchronized (LockDemo.class) {
        System.out.println("累加前num的值为" + num);
        num++;
        Thread.sleep(1);//加一个sleep防止编译器优化,2条语句可以被优化成num += 2;
        num++;
        System.out.println("累加后num的值为" + num);
    }
}

这里用了synchronized关键字,这个关键字可以让同一时刻,只有一个线程执行代码块,所以很好的解决了数据一致性问题,加了synchronized之后的console:

...
...
累加后num的值为196
累加前num的值为196
累加后num的值为198
累加前num的值为198
累加后num的值为200
num =200

使用reentrantLock

java中也可以使用reentrantLock对象来执行加锁操作(推荐)

定义一个成员对象:

private static ReentrantLock reentrantLock = new ReentrantLock();

在需要加锁的地方使用lock()和unlock()

public static void doSomethings() throws InterruptedException {
    reentrantLock.lock();
    System.out.println("累加前num的值为" + num);
    num++;
    Thread.sleep(1);//加一个sleep防止编译器优化,2条语句可以被优化成num += 2;
    num++;
    System.out.println("累加后num的值为" + num);
    reentrantLock.unlock();
}

volatile

可能有的人觉得用锁太消耗资源,实时是这样的,因为加锁之后,底层实现上会加总线锁的操作,这种情况下,cpu不能访问内存,所以其他线程不能做任何操作.所以说加锁代价太大,所以这里volatile可以避免部分加锁操作

这里说的是部分,也就是说volatile并不能代替加锁,因为volatile修饰的变量相当于每次都去内存取,而不会在缓存中去,我们知道cpu有高速缓存,就是优化一些经常访问的数据.但是在这个例子中,线程取了num的值,就算取的一瞬间num值是最新的,但是有可能,刚取完,这个线程的被分配的时间片用完了,过了很久这个线程才重新被唤醒,然后执行累加操作,这个时间就尴尬了...

加了volatile修饰之后的结果

累加后num的值为194
累加后num的值为195
累加后num的值为196
累加后num的值为197
累加后num的值为198
累加后num的值为199
num =199

我执行了好几遍,发部分都是返回200,但是还是存在不是200的情况,如上,也就是说volatile治标不治本.

CAS

用锁也不好,不用锁也不好,那怎么办啊,难道使用多线程必须付出很大的代价吗,其实不是,还有一个非常好的方法,不用加锁都能做到数据一致性,也就是CAS算法,什么是CAS呢,举个例子:

如果有A,B,C这3个线程同时操作num,假如num如果现在是100,那么A线程,B线程,C线程都读取了num,那A,B,C都是持有100这个数字,这时B线程做了累加操作,然后修改了num为100,在修改的时候做一个CAS的操作,也就是比较一下num的值,比较什么东西呢,B在修改num的时候需要比较num是不是100,因为B是把100改成了101,所以如果B碰到了num不是100的情况那就是有问题了,如果num当前不是100,B强行把num改成了101,那最后结果肯定有问题.因为中间某些操作被B给掩盖了.碰到问题了怎么办呢,最好的办法就是丢弃这次操作,然后重新读取num的值做累加操作.

代码实现
因为CAS是需要CPU的机器指令支持的,所以代码上我用一个锁来模拟这个指令

public static void doSomethings() throws InterruptedException {
    System.out.println("累加前num的值为" + num);
    int olnNum = num;
    int newNum = num + 1;

    //* 模拟cas,并做2遍操作,因为一边操作意外重现率太低,所以这里为了演示做2遍
    boolean loop = true;
    while (loop) {
        int currentNum = num;
        synchronized (LockDemo.class) {
            if (olnNum == currentNum) {
                num = newNum;
                loop = false;
            }
        }

        //** 失败后重新读取
        if (loop) {
            System.out.println("currentNum = " + currentNum + ",但是oldNum = " + olnNum);
            olnNum = num;
            newNum = num + 1;
        }
    }

    //* 第二遍
    loop = true;
    while (loop) {
        int currentNum = num;
        synchronized (LockDemo.class) {
            if (olnNum == currentNum) {
                num = newNum;
                loop = false;
            }
        }

        //** 失败后重新读取
        if (loop) {
            System.out.println("currentNum = " + currentNum + ",但是oldNum = " + olnNum);
            olnNum = num;
            newNum = num + 1;
        }
    }

    System.out.println("累加后num的值为" + num);
}

console

累加后num的值为197
累加后num的值为164
currentNum = 162,但是oldNum = 161
currentNum = 161,但是oldNum = 160
累加后num的值为199
累加后num的值为157
累加后num的值为156
currentNum = 151,但是oldNum = 150
累加后num的值为200
累加后num的值为198
累加后num的值为196
累加后num的值为193
累加后num的值为191
累加后num的值为190
累加后num的值为187
num =200

tip

有时候光比较值也是不够的,比如比较之后发现old和预期的一样,但是实际上中途有一个加操作和减操作,导致old和预期的一样,所以实际上在比较的时候应该额外加一个版本号的比较,版本号能够体现数据是否中途有被更新过,比如每次更新之后版本号+1,这样可以保证比较的时候是不是预期的版本

上面就是对CAS一个用户态的实现,仅仅是一个模型而已,方便大家理解.

相关文章

  • 初探CAS

    现在的工作中,多线程肯定是经常挂嘴边的,实际上我们在做项目过程中,去写多线程的情况其实没多少,一般这些操作都是被封...

  • CAS初探

    本文首发:WindCoder 什么是CAS? 全称:Compare And Swap,翻译为比较并替换。 CAS机...

  • CAS第一天入门

    1. CAS的直观认识 主要参考:CAS的官网CAS的视频教程 CAS的结构 a) CAS Server 用于完成...

  • CAS实现SSO单点登录原理

    CAS 简介 CAS 初识 CAS : Central Authentication Service 开源的项目;...

  • CAS实现SSO单点登录原理

    1. CAS 简介 1.1. What is CAS ? CAS ( Central Authenti...

  • CAS实现SSO单点登录原理

    1. CAS 简介 1.1. What is CAS ? CAS ( Central Authenti...

  • JUC(三)CAS与原子变量

    一、CAS 1、CAS简介 CAS(compare and swap),比较并交换。 CAS 操作包含三个操作数 ...

  • php cas单点登录

    一、CAS简介1、结构体系从结构体系看, CAS 包括两部分: CAS Server 和 CAS Client 。...

  • CAS+ABA+Unsafe+悲观锁和乐观锁

    1 CAS CAS,即compare and swap。CAS操作是原子操作,在多线程中执行CAS操作可以实现同步...

  • 苹果 ARKit 初探

    苹果 ARKit 初探 苹果 ARKit 初探

网友评论

      本文标题:初探CAS

      本文链接:https://www.haomeiwen.com/subject/asgoxftx.html