Java-并发编程之可见性与原子性
文章目录
一、并发编程-什么是可见性?
要了解Java并发编程内存可见性之前,先来简单了解一下JMM即Java内存模型(Java memory model)。
1.1-Java并发共享内存模型
(1)Java所有变量都存储在主内存中;
(2)每个线程都有自己独立的工作内存,里面保存该线程的使用到的变量副本(该副本就是主内存中该变量的一份拷贝)
1.2-CPU的缓存机制
我们知道Java的内存机制之后再来看,cpu由于处理速度一般会比主存(也就是主板上插着的内存条)快很多,为了避免cpu一直快车等慢车的情况发生。所以cpu特地设置了缓存位置被称为local cache。一般分为三级,分别为L1-L3,可以在windows的任务管理器中查看到。一般L1缓存是cpu单个核心独占的缓存,L2缓存一般是2个核心公用一个,L3缓存一般是四个核心或者八个核心共用一个(这个具体要看cpu的设计方式);并且L1-L3缓存的速度依次降低。
Jvm内存模型中,不同线程拥有各自的私有工作内存(栈区、本地方法栈、程序计数器),当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的工作内存的变量副本中,当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值。
1.3-举例说明
-
首先申明一个变量a,当单一线程时,我对变量进行修改之后在进行输出,可以正确输出修改之后的变量值false。这种下面一行代码对之前代码的可见性时显而易见的。
-
此时我们再添加一个线程,尝试在子线程中获取变量值的情况。
代码
public class Concurrent {
static boolean a = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(a){}//因为线程启动开销大,时间长
}).start();
Thread.sleep(1000) ;//防止线程在运行到while(a)时,a已经被赋值false。
a = false;
System.out.println(a);
}
}
正常分析来说首先变量a为true,启动线程之后由于等待1s之后a才被赋为false。那么子线程应该先进入while循环,之后由于a被赋值不满足循环条件而退出。但是执行结果却如下所示:
虽然打印输出了false,但是此时子线程B还没有结束,说明线程B获取的还是false。
原因分析:此时就引出了今天的话题线程之间的不可见性–>及线程A修改的变量值,线程B不能马上获取到最新值导致线程B一致死在循环中出不来。
那为什么这么久还不能更新呢?因为是空语句导致,线程没有机会去主存中加载新的值,只要修改线程循环体,哪怕只是sleep 1ms,都可以使线程B有喘息时间去获取最新值。
解决1:
public class Concurrent {
static boolean a = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(a){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(1000) ;
a = false;
System.out.println(a);
}
}
1.4 -解决可见性
面对问题:线程A写变量,线程B读变量场景。
添加volatile关键字,使变量的值在主存和local cache之间强制刷新。
volatile的实现原理:
1. 当一个线程修改了共享变量,CPU的嗅探机制会发现副本与主内存数据的不一致,通过汇编lock前缀指令,锁定共享变量主内存区域,并将新值写回到主内存;
2. 写回内存的操作会使其他CPU中缓存了该数据的地址失效。(MESI协议,即缓存一致性协议)
代码:
public class Concurrent {
static volatile boolean a = true;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while(a){
// try {
// Thread.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}).start();
Thread.sleep(1000) ;
a = false;
System.out.println(a);
}
}
执行结果:
二、并发编程-什么是原子性?
2.1-问题引入
代码
public class Concurrent {
static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
for(int i =0;i<100000;i++){
new Thread(()->{
a++;
}).start();
new Thread(()->{
a++;
}).start();
}
System.out.println(a);
}
}
这里开辟了两条线程,分别每个线程对原始数字a进行++操作一万次.
执行结果:
理想的结果因该是每个线程++1万次,最终输出结果为20000。但是实际的结果确实输出的数值总是小于等于两万次,并且这里的变量a已经通过volatile修饰强制刷新了,那每次结果可能不尽相同这是什么原因呢?
2.2-问题分析
自增操作a++过程分析:
第一步:需要去主存中取到当前变量的值;
第二步:将当前获取的变量值进行自增;
第三步:将更改过的变量值重新刷写回主内存。
看似简单的一步操作其实是包含这三个步骤的,那么上面的问题是怎么发生的?
实质上由于多线程两个线程执行之间独立进行,并没有明显的执行先后顺寻。那么这两个线程在执行时候,是存在同时获取到变量的值并进行++操作的可能的,按上结果分析就是两万次中刚好有9次卡在了,两个线程同时获取了变量值并进行++操作,导致本该++两万次由于同时操作而变成了19991次。这里就算是volatile同样不能保证准确,因为volatile只能保证一个写一个读,读的能获取到最新值,而这里是既有读操作又有基于读操作的写操作。
**如何解决:**将读操作和写操作合为一步,要么同时发生,要么同时不发生。
2.3- 原子性与原子类
原子性就是计算机中的一个不能再分割的动作,要么做完、要么不做,中间不会被打断。
==面对问题:==多线程之间既有读操作也有基于读操作的写操作。
---->使用java中的Atomic类实现可见性。
import java.util.concurrent.atomic.AtomicInteger;
public class Concurrent {
static AtomicInteger a = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
for(int i =0;i<100000;i++){
new Thread(()->{
a.getAndAdd(1);
}).start();
new Thread(()->{
a.getAndAdd(1);
}).start();
}
Thread.sleep(1000);
System.out.println(a);
}
}
··· 保证原子性的前提条件是一定要保证可见性的,那这里为什么AtomicInteger为什么不使用volatile关键字修饰呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0IylvH7M-1624955866007)(Java-并发编程之可见性与原子性/image-20210629155028400.png)]
进入内部我们可以看到,Atomic在实现的时候,value值就已经使用volatile进行修饰了。
这里顺带提一点:
Volatile不具有传递性,用volatile修饰的对象的内部属性不具有可见性;
反之用volatile修饰的内部属性也不能保证所在对象的可见在性。
2.4- Synchronized关键字
也可以使用sync关键字来保证原子性的实现。
import java.util.concurrent.atomic.AtomicInteger;
public class Concurrent {
static AtomicInteger a = new AtomicInteger(0);
static int b = 0;
public static void main(String[] args) throws InterruptedException {
for(int i =0;i<100000;i++){
new Thread(()->{
// a.getAndAdd(1);
synchronized (Concurrent.class){
b++;
}
}).start();
new Thread(()->{
// a.getAndAdd(1);
synchronized (Concurrent.class){
b++;
}
}).start();
}
Thread.sleep(1000);
System.out.println(b);
}
}
执行结果:
这里使用了同步代码块,同一时间只有一个线程可以进入代码段,使用sync强制保证原子性。
三、总结
-
**量级:**volatile<Atomic<Synchronized;
-
volatile可以保证可见性,一个线程写,另一个线程读,保证读取最新值;
-
Atomic可以保证原子性(包含可见性),既有读操作又有写操作时;
-
Synchronized可以保证整个代码块中的操作都是原子性的