Java:CAS(乐观锁)

摘要:
CAS操作乐观锁。它不是每次都锁定,而是假设没有冲突并完成操作。如果冲突失败,它会重试,直到成功。尽管Java 1.6已经优化了Synchronized,以增加从偏重锁到轻量级锁,然后再到重量级锁的转换,但在最终转换为重量级锁之后,性能仍然很低。原子操作类引用java。实用程序。concurrent在atomic包下,一系列包装类以atomic开头。privatestaticAtomicIntegercount=newAtomicInteger;publicstaticvoidmain{for{newThread.start();}尝试{Thread.sleep;}catch{e.printStackTrace();}系统输出。println;}使用AtomicInteger后,最终输出也可以保证为200。CAS属于乐观锁。乐观的是,程序中的并发性并不是那么严重,所以线程一直在尝试更新。

本文讲解CAS机制,主要是因为最近准备面试题,发现这个问题在面试中出现的频率非常的高,因此把自己学习过程中的一些理解记录下来,希望能对大家也有帮助。

什么是悲观锁、乐观锁?在java语言里,总有一些名词看语义跟本不明白是啥玩意儿,也就总有部分面试官拿着这样的词来忽悠面试者,以此来找优越感,其实理解清楚了,这些词也就唬不住人了。

  • synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
  • CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

在进入正题之前,我们先理解下下面的代码:

 private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每个线程让count自增100次
                    for (int i = 0; i < 100; i++) {
                        count++;
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }

请问cout的输出值是否为200?答案是否定的,因为这个程序是线程不安全的,所以造成的结果count值可能小于200;

那么如何改造成线程安全的呢,其实我们可以使用上Synchronized同步锁,我们只需要在count++的位置添加同步锁,代码如下:

private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每个线程让count自增100次
                    for (int i = 0; i < 100; i++) {
                        synchronized (ThreadCas.class){
                            count++;
                        }
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }

加了同步锁之后,count自增的操作变成了原子性操作,所以最终的输出一定是count=200,代码实现了线程安全。

但是Synchronized虽然确保了线程的安全,但是在性能上却不是最优的,Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如AtomicBooleanAtomicIntegerAtomicLong。它们分别用于BooleanIntegerLong类型的原子性操作。


    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每个线程让count自增100次
                    for (int i = 0; i < 100; i++) {
                        count.incrementAndGet();
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }

使用AtomicInteger之后,最终的输出结果同样可以保证是200。并且在某些情况下,代码的性能会比Synchronized更好。

而Atomic操作的底层实现正是利用的CAS机制,好的,我们切入到这个博客的正点。

什么是CAS机制

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

这样说或许有些抽象,我们来看一个例子:

1.在内存地址V当中,存储着值为10的变量。

Java:CAS(乐观锁)第1张
image

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

Java:CAS(乐观锁)第2张
image

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

Java:CAS(乐观锁)第3张
image

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。

Java:CAS(乐观锁)第4张
4.jpg

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

Java:CAS(乐观锁)第5张
image

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

Java:CAS(乐观锁)第6张
image

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

Java:CAS(乐观锁)第7张
image

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

看到上面的解释是不是索然无味,查找了很多资料也没完全弄明白,通过几次验证后,终于明白,最终可以理解成一个无阻塞多线程争抢资源的模型。先上代码

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author hrabbit
 * 2018/07/16.
 */
public class AtomicBooleanTest implements Runnable {

    private static AtomicBoolean flag = new AtomicBoolean(true);

    public static void main(String[] args) {
        AtomicBooleanTest ast = new AtomicBooleanTest();
        Thread thread1 = new Thread(ast);
        Thread thread = new Thread(ast);
        thread1.start();
        thread.start();
    }
    @Override
    public void run() {
        System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
        if (flag.compareAndSet(true,false)){
            System.out.println(Thread.currentThread().getName()+""+flag.get());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag.set(true);
        }else{
            System.out.println("重试机制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            run();
        }

    }
}

输出的结果:

thread:Thread-1;flag:true
thread:Thread-0;flag:true
Thread-1false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重试机制thread:Thread-0;flag:false
thread:Thread-0;flag:true
Thread-0false

这里无论怎么运行,Thread-1、Thread-0都会执行if=true条件,而且还不会产生线程脏读脏写,这是如何做到的了,这就用到了我们的compareAndSet(boolean expect,boolean update)方法
我们看到当Thread-1在进行操作的时候,Thread一直在进行重试机制,程序原理图:


Java:CAS(乐观锁)第8张
image

这个图中重最要的是compareAndSet(true,false)方法要拆开成compare(true)方法和Set(false)方法理解,是compare(true)是等于true后,就马上设置共享内存为false,这个时候,其它线程无论怎么走都无法走到只有得到共享内存为true时的程序隔离方法区。

看到这里,这种CAS机制就是完美的吗?这个程序其实存在一个问题,不知道大家注意到没有?

但是这种得不到状态为true时使用递归算法是很耗cpu资源的,所以一般情况下,都会有线程sleep。

CAS的缺点:

1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。


作者:AubreyXue
链接:https://www.jianshu.com/p/ae25eb3cfb5d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

免责声明:文章转载自《Java:CAS(乐观锁)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇英语人称代词、物主代词用法口诀!超全~IIS部署ASP.NET网站后提示只有在配置文件或 Page 指令中将 enableSessionState 设置为 true 时,才能使用会话状态...下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

我看到的最棒的Twisted入门教程!

http://www.douban.com/note/232204441/ http://www.cnblogs.com/sevenyuan/archive/2010/11/18/1880681.html http://blog.csdn.net/hanhuili/article/details/9389433 我看到的最棒的Twisted入门教程! ht...

Python-GIL 进程池 线程池

5、GIL vs 互斥锁(*****) 1、什么是GIL(Global Interpreter Lock) GIL是全局解释器锁,是加到解释器身上的,保护的就是解释器级别的数据 (比如垃圾回收的数据) 同一个进程内的所有线程都需要先抢到GIL锁,才能执行解释器代码 2 为什...

精选30道Java多线程面试题

1、线程和进程的区别  进程是应用程序的执行实例。比如说,当你双击的Microsoft Word的图标,你就开始运行的Word的进程。线程是执行进程中的路径。另外,一个过程可以包含多个线程。启动Word时,操作系统创建一个进程并开始执行该进程的主线程。 由于一个进程可以由多个线程,线程可以被认为是“轻量级”的过程。因此,一个线程和一个进程之间的本质区别在于...

【转载】.net题目 —— 加了一些东西

转自:http://www.cnblogs.com/tuyile006/archive/2007/03/20/681154.html 1:a=10,b=15,在不用第三方变量的前提下,把a,b的值互换2:已知数组int[] max={6,5,2,9,7,4,0};用快速排序算法按降序对其进行排列,并返回数组3:请简述面向对象的多态的特性及意义!4:sess...

Delphi线程基础知识

参考http://blog.chinaunix.net/uid-10535208-id-2949323.html 一、概述 Delphi提供了好几种对象以方便进行多线程编程。多线程应用程序有以下几方面的功能: 1.避免性能瓶颈:单线程应用程序在进行比较慢的操作如磁盘读写的时候,CPU必须停下来等待,直到该操作执行完毕。而多线程应用程序在进行比较慢的操作如磁...

Linux线程同步之读写锁

1. 特性:     一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性, 当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞. 当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须阻塞知...