JUC之原子性操作

Posted Huterox

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC之原子性操作相关的知识,希望对你有一定的参考价值。

文章目录

前言

最近是真的事情比较多,所以一鸽再鸽。而且目前的进度也非常不理想,进展缓慢。今天主要是更新一下以前立下的flag,更新这个java JUC的内容。慢慢来,后面再深入JVM,同时Spring源码也在同步推进当中,目前已经有想法打造一个类Spring框架(基于Spring核心原理实现,SmartBean的文档已经开始编写~,什么时候能用那就是我鸽到不能在鸽了(狗头))

原子性

在说说这个JUC原子性之前,我们得聊聊啥是原子性,这个其实和我先前在数据库的内容里面说的类似,就是不可在分割,在我们的多线程里面就是相当于一把锁,在当前的线程没有完成对应的操作之前,别的线程不允许切换过来,因为咱们的CPU是来回切换的嘛,操作系统也是多道处理的嘛。

举个栗子。非原子性

你看这个线程1都没有加完(饭都没吃完)突然切换给了线程2,这样对于count这个加加操作不就废了嘛。

可见性

说完了,原子性,咱们再来说说可见性,也就是这个关键字

volatile

先来看看在百度上的解释

精确地说就是,编译器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

1)并行设备的硬件寄存器(如:状态寄存器)

2)一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

3)多线程应用中被几个任务共享的变量

这是区分C程序员和嵌入式系统程序员的最基本的问题:嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所有这些都要求使用volatile变量。不懂得volatile内容将会带来灾难。

假设被面试者正确地回答了这个问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是真正懂得volatile完全的重要性。

在我们java里面就是在线程通信的时候,防止JVm优化,也就是说JVM里面有个缓存一样的东西,每次当前线程都会直接从缓存里面读取这个数据,造成,线程之间通信不及时(可以先这样粗略了解)。这就会导致一个问题,当对于一个公共的变量,在A线程进行修改之后,B线程可能无法及时捕获到变量的修改,从而导致通信失败。(当然我不确定现在JVM是不是会主动避免这种情况,毕竟我们现在学习,开发SpringBoot项目都还是1.8,现在连eclipse都要11了)。

下面我还是举个例子。

public class VoltStaticTest 
    public static void main(String[] args) throws InterruptedException 
        Test test = new Test();
        test.start();
        Thread.sleep(2000);//2秒后设置flag为false
        test.setFlag(false);
    

    static class Test extends Thread
        boolean flag = true;

        public boolean isFlag() 
            return flag;
        

        public void setFlag(boolean flag) 
            this.flag = flag;
        

        public void go()
            System.out.println("循环开始");
            while (flag)
            System.out.println("循环结束");
        

        public void run()
            go();
        
    

按照我们的理解是2s之后那个循环要结束,但是这里会一种堵塞。

但是你这样

volatile boolean flag = true;

就不会

这里一定要注意的是,这个关键字只是保证了,可见性,并不是原子性。

volatile 没有原子性

这个其实很好理解,但是不知道为什么总是有人乱搞。
同样,我们演示一下代码。

public class VoltStaticTest 
    public static void main(String[] args) 
        for (int i = 0; i < 100; i++) 
            new Test().start();
        

    


    static class Test extends Thread
        volatile static int count = 0;
        public static void add()
            for (int i=0;i<100;i++)
                count++;
            
            System.out.println(count);
        
        public void run()
            add();
        
    

可以看到,我们让一百个线程,每个线程负责对count执行加加操作,每个线程加100次,加完一100次之后,输出count,正常情况下,如果它具备原子性,那么输出的值应该是整数,整数输出(几百,几千输出)
但是实际效果是

所以可见,不行。
如果要具备原子性应该是这样的。


现在我们把那个可见性关键字去掉

所以我们其实可以发现使用了 synchronized 其实是同时具备了原子性,可见性。

原子类

这个其实没啥好说的,有时候,我们就想要累加是吧,那我们直接使用那个原子类。

new AtomicInteger();

但是你仔细找一找好像没有那个什么同步的玩意,这是为啥,挖个坑,下面就知道了。

CAS

我们仔细分析一下那个I++的操作,发现这里其实有三部分

1. 读取I的值

2. 操作I的值

3. 把I的值写入到主内存

如果我们直接启动线程,使用同步异步的话,这样的线程开销是比较大的,所以,我们这里有一个神奇的东西叫做CAS(compare And Swap)这个是由硬件实现的,开销更小,能干嘛呢,其实也是原子性操作嘛。实现三个东西

read - modify -writer

原理

把数据更新到主内存当时,在此读取内存当中的值,如果这个内存当中的值(现在)和原来期望的值,那么我们就更新。其实这个和乐观锁有点像,只是人家是通过那个版本号对比滴。
既然如此咱们就来好好聊聊这个原理。(当然不是底层哈)

乐观锁

首先,这个乐观锁,其实不是一个线程安全的,但是通过这个巧妙的关系,我们可以实现那种类似同步的操作。前面我们使用synchronized是直接把代码块锁了,这个其实是叫做,互斥锁,也叫作悲观锁。

好,我们先来聊聊悲观锁,这个很好理解。

悲观锁

聊完了,悲观锁,那么我们说说乐观锁,这个乐观锁和我们的CAS其实类似。

我们先来说说,没有用乐观锁的时候,会出现啥

你看,这样不就完犊子了嘛。
所以如何解决,我们乐观锁就是加个版本号。


好了现在可以聊聊啥是CAS了

CAS原理

这个和乐观锁没啥区别,只不过,人家不用版本号,直接对比值就行了。

所以人家叫啥,叫CAS。

代码实现

又到了最激动人心的时候了。
(比算法简单多了~)


public class CASCount 
    //使用CAS算法实现的计数器
    private volatile long count=0;

    public CASCount() 
    

    public CASCount(long count) 
        this.count = count;//默认从0开始
    


    public boolean CompareandSwap(long nowcount,long afteraddcount)
        if(count==nowcount)
            count = afteraddcount;
            return true;
        else 
            return false;
        
    

    public long IncreamentwithGet()
        //自增,并且返回值
        long nowcount;
        long afteraddcount;
        do 
            nowcount = count;
            afteraddcount = nowcount+1;
        while (!CompareandSwap(nowcount,afteraddcount));
        return afteraddcount;
    


class TestCAS
    public static void main(String[] args) 
        CASCount casCount = new CASCount(0);
        for (int i = 0; i < 100; i++) 
            new Thread(()->
                System.out.println(casCount.IncreamentwithGet());
            ).start();
        
    

完美。

填坑原子类AtomicInteger

在了解了CAS之后,咱们再来看看那个这个AtomicInteger源码

现在这个玩意的实现就不用我多说了吧。它的实现就是CAS

ABA问题

我们自己实现了那个原子类计数器,显然只是考虑了,理想情况下。但是显示情况下,没那么好运。

10000次又不行了。

为什么,我们来分析一下。
我们是什么直接对比,那个,如果当前的值,和我以前获取的值不一样,那么我们就那啥交互写入。但是有时候,我们的线程的速度都是不一样的。

假设有那么一个时刻。
A线程更新为 10
B线程更新为 20
C线程更新为 10

我们只是单纯地比对,所以此时C线程的更新是会成立的,那么问题来了,C更新了,那么B的更新不就废了嘛,所以我们发现10000次操作最后的值没有10000.
就是这个原因。

那么这个就是ABA问题(值又回去了)
那怎么解决呢,其实对比乐观锁的流程,我们已经知道了答案,没错,就是给个版本号呀!

又到了,快乐的代码时刻。

首先其实有两个解决方案,第一个最简单,就是直接加一把锁,我们原来出现ABA的问题是因为那个线程的通信问题,个别线程的操作比较慢,产生延时,有些线程比较快先操作完成。所以问题就是如何解决线程速度不均衡的问题。所以第一个方案,就是强行让它“均匀”

之后就是我们版本号的问题。

我们现在这样做

public class CASCount 
    //使用CAS算法实现的计数器
    private volatile long count=0;
    private volatile long version=0;//版本号就是从0开始的
    public CASCount() 
    

    public CASCount(long count) 
        this.count = count;//默认从0开始

    


    public boolean CompareandSwap(long nowversion,long afteraddcount)

        if(nowversion>version)
            count = afteraddcount;
            version = nowversion;
            return true;
        else 
            return false;
        
    

    public long IncreamentwithGet()
        //自增,并且返回值
        long nowversion;
        long afteraddcount;
        do 
            nowversion = version;
            afteraddcount = count+1;
            nowversion = nowversion+1;
        while (!CompareandSwap(nowversion,afteraddcount));
        return afteraddcount;
    


class TestCAS
    public static void main(String[] args) 
        CASCount casCount = new CASCount(0);
        for (int i = 0; i < 10000; i++) 
            new Thread(()->
                System.out.println(casCount.IncreamentwithGet());
            ).start();
        
    

咋一看貌似解决了问题,但是实际上的话,你会发现版本号也会出现ABA问题,所以这就尬了,咱们得那啥,再判断。
所以ABA的问题在于不安全version还是会切换,这就麻烦了。这个时候咋办咧~明天更新。

以上是关于JUC之原子性操作的主要内容,如果未能解决你的问题,请参考以下文章

Juc11_Java内存模型之JMM八大原子操作三大特性读写过程happens-before

JUC - 多线程之 CAS和原子类

JUC并发编程 共享模型之内存 -- Java 内存模型 & 原子性 & 可见性(可见性问题及解决 & 可见性 VS 原子性 & volatile(易变关键字))(代码

线程安全之原子操作

深入理解java:2.3.1. 并发编程concurrent包 之Atomic原子操作

1.JUC锁的一些概念