初探CAS

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

使用锁

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

看一段没有锁的代码(使用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一个用户态的实现,仅仅是一个模型而已,方便大家理解.

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容