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