美文网首页java学习之路
Java Util Concurrent并发编程(六)CAS和j

Java Util Concurrent并发编程(六)CAS和j

作者: 唯有努力不欺人丶 | 来源:发表于2020-11-22 22:56 被阅读0次

    深入理解CAS

    什么是CAS?CAS的全称是compareAndSet,比较并交换
    一般通用的用法就是如果对象是期望的,那么就更新成给定的第二个。在原子类型的类中可以查看一下:

    如图两个参数,是期望值和更新值
    两个语句运行结果对比
    其实大家记不记得我们之前就说过了,原子类是基于内存操作的。而众所周知的java不能操作内存啊?这是怎么回事呢?不得不提一个比较神奇的类,Unsafe类。
    其实这个名字起的就很霸道,不安全的类。但是既然不安全为什么还要有这个类呢?那就说明了这个类的重要性了。这个类到底是干什么的呢?
    JAVA无法操作内存。JAVA可以调用C++(native)。C++可以操作内存。而这个Unsafe类是java留的一个后门,可以通过这个类操作。
    我们可以点进去看,Unsafe类里都是native方法。
    CAS底层代码
    CAS:比较当前工作内存中的值和主内存中的值。如果这个值是期望的则执行这个操作,否则一直循环。
    • 缺点:
      • 循环会耗时
      • 一次只能保证一个共享变量的原子性
      • ABA问题

    ABA问题:简单来说,就是如果一个值是1, A线程CAS(1,2)将值改成2了。再CAS(2,1)将值改成1了。 这个时候B线程CAS(1,3)其实这个期望的1是最开始时候的1.而不是被A线程重置回来的1。虽然本质上得到的还是1,看似不影响什么。。但是其实差别还挺大的。看视频的时候一条弹幕特别好的解释了这种情况:你看上一个清纯可爱天真善良的女孩。然后打算追她。中间一段时间她交了好几百个男朋友。现在你要追到她了,女孩还是这个女孩没有变,但是请问她还是你想要的那个她了么?
    原子引用
    问题所如何解决这个ABA问题呢?其实以我们现有的知识也挺容易实现的。乐观锁其实就可以实现。在这个值的每次改动的时候记录一个版本号。并且在每次调用获取上次改动的版本号。这样我们就能知道本次获取到的对象除了值以外,是不是我们想要的那个。
    其实实现的方法也简单的很,就是这个CAS方法多两个参数:一个是当前预期值和当前版本号。一个是想要更新值和想要更新的版本号。
    比对的结果就不是单纯的值是不是相等了,也要判断版本号是不是相等。
    同时如果值发生更新以后,也会更新版本号。
    当然了,这个版本号是我习惯性的叫法,外国人一般都用邮票来表达这个意思的。我们去官方手册找到这个类的介绍:

    带版本号的原子类
    截图下面是有个构造器方法参数中initialStamp可以理解为初始版本号。下面是这个方法的代码实现:
        //CAS是compareAndSet  比较并交换
        public static void main(String[] args) {
            AtomicStampedReference<Integer> integer = new AtomicStampedReference<Integer>(122, 1);
            System.out.println(integer.compareAndSet(122, 123, 1, 2));
            System.out.println(integer.getStamp());
        }
    
    版本号变成2了

    这个demo中有个坑,就是如果泛型是包装类。比如Integer 2020和 2020本身不是一样的。稍微有经验的也知道包装类的等于也要equals。不过工作中很少用这种类型。
    反正利用这个原子引用就可以解决这个ABA问题了。

    各种锁的理解

    公平锁和非公平锁:

    • 公平锁:非常公平,不能插队。严格按照线程的先来后到。
    • 非公平锁:每一次获取锁都要抢,各种插队。但是设置非公平锁是为了公平。(synchronized和lock默认都是非公平的。比如A,B线程在等一把锁。A要执行两个小时,B要执行1秒钟。如果让B为了这一秒钟等两个小时本身就是有问题的)
      这个我们可以在代码中看一下,常用的Lock锁的构造器:
      无参默认非公平。有参并且是true才是公平锁
      可重入锁:
      这个其实挺好理解的,有时候汉字的好处就是见名可以知意。 其实这个就是当拿到这个锁了就可以开所有这个锁的门。下面一个demo:
    public class D1 {
        public static void main(String[] args) throws Exception {
            Phone phone = new Phone();
            for (int i = 0; i < 1000; i++) {
                new Thread(()->{
                    phone.sms();
                },"线程"+i).start();
            }       
        }
    }
    class Phone{
        public synchronized void sms() {
            System.out.println(Thread.currentThread().getName()+"sms");
            call();
        }
        public synchronized void call() {
            System.out.println(Thread.currentThread().getName()+"call");
        }
    }
    

    其实这个demo是想说明:sms和call都上锁的。然后我们又在sms中调用call。我特意跑了1000个线程,大家也可以试一下,结果集中一个线程的sms和call一定是一起执行的。按照我们正常的逻辑,执行完发短信后因为默认是非公平锁,应该大家都抢锁,怎么可能每次都sms和call都一起执行?
    问题的答案只有一个。执行完了sms后,线程根本没释放锁,直接带着锁进入了call方法,所以说相当于我门在线程中这一个锁开了好几个门。而且这个demo是用synchronized,下面我们再用Lock试试。


    demo运行结果

    Lock版本看似代码和synchronized差不多,但是让我们能更明确的看懂这个锁机制。下面直接附上代码:

    public class D1 {
        public static void main(String[] args) throws Exception {
            Phone phone = new Phone();
            for (int i = 0; i < 1000; i++) {
                new Thread(()->{
                    phone.sms();
                },"线程"+i).start();
            }       
        }
    }
    class Phone{
        Lock lock = new ReentrantLock();
        public void sms() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"sms");
                call();
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                lock.unlock();
            }
        }
        public void call() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"call");
            } catch (Exception e) {
                // TODO: handle exception
            }finally {
                lock.unlock();
            }
        }
    }
    

    敲黑板!上面的代码运行结果和synchronized的一样,所有我就不截图了。但是这个代码因为Lock要上锁和解锁,所以其实我们可以很明确的看出了sms中调用call,是上了两次锁并且解了两次锁。其本质应该如下:
    lock1上锁->lock2上锁->lock2解锁->lock1解锁
    这四个步骤我们随便在其中一个方法上少写一句都会造成错误的。而可重入的理念在这里也能看的更加清楚:其实这个lock1,lock2是一把锁。
    而当sms拿到lock这个锁以后,在没有释放的前提下,还可以用这个锁进去到了call方法中。所以这个可重入是不是说的很形象?
    注意:lock中加锁解锁是一对操作,一定要注意。看我下面代码:

    image.png
    这样的代码在call不单独调用的前提下,运行是完全没问题的。因为本质上还是加两次锁解两次锁。由此说明lock是针对锁的加锁解锁计数。

    自旋锁:
    不断循环判断,直到获得锁。
    这个说真的,我都觉得没啥代码可说的。因为我们上面再cas的时候看底层源码其实就用到了自旋锁:

    自旋锁
    看到了没?do{}while()句式。这个while里加自己的判断。这里源码中是说当第一个线程进来了肯定是满足cas的。所以直接return了。但是如果是多线程的时候并发,第一个没执行完之前,肯定是不满足cas的,所以一直卡在do-while中,直到A执行完走了,就满足CAS了,所以B可以正常执行了。
    我觉得这个没啥好说的。理论很简单,实际没用过,反正就这样,直到什么是自旋锁就行了。
    死锁:
    其实这个情况最好说了,也好理解。就是线程A有的锁1,等锁2。线程B有了锁2等锁1.就这样两个线程互相等着,就等死了。这种demo也很好写,我直接附上代码:
    public class D1 {
        public static void main(String[] args) throws Exception {
            Phone phone = new Phone();
            new Thread(()->{phone.sms();},"A").start(); 
            new Thread(()->{phone.call();},"B").start();    
        }
    }
    class Phone{
        Lock lock1 = new ReentrantLock();
        Lock lock2 = new ReentrantLock();
        public void sms() {
            lock1.lock();
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"sms");
                call();
            }catch (Exception e) {
                
            }finally {
                lock1.unlock();
            }
        }
        public void call() {
            lock2.lock();
            try {
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName()+"call");
                sms();
            }catch (Exception e) {
                // TODO: handle exception
            }finally {
                lock2.unlock();
            }   }
    }
    

    这个线程执行不完了。卡在lock1等lock2.lock2等lock1的怪圈里。。
    这里重点不说什么是死锁。主要是说怎么解决。
    这里不得不说JDK的bin目录下的一个很实用的工具:jps

    image.png
    这个可以查看当前线程:命令是jps -l(可以查看正在运行的线程)
    image.png
    其实这里jps的命令可以自己去看的。这个工具毕竟是JDK自带的。感兴趣的可以深入了解一下。
    自此,JUC的东西我就看完了,也记完了。其实这里面东西不多,但是很杂,这一块那一块的。但是其实干货不少,实际中能用到的也挺多的。一共是八个多小时,我零零落落看了三周。自我感觉是用了双倍甚至三倍的时间消化。反正算是完成了我今年计划中的一项了,接下来就是spring boot源码和netty代表的nio。有时间再学es。在这里也给自己打打气,学到即得到!也祝大家工作中保持进步,一起共勉!
    本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利!

    相关文章

      网友评论

        本文标题:Java Util Concurrent并发编程(六)CAS和j

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