java架构之路(多线程)JMM和volatile关键字(二)
貌似两个多月没写博客,不知道年前这段时间都去忙了什么。
好久以前写过一次和volatile相关的博客,感觉没写的那么深入吧,这次我们继续说我们的volatile关键字。
复习:
先来简单的复习一遍以前写过的东西,上次我们说了内存一致性协议M(修改)E(独占)S(共享)I(失效)四种状态,还有我们并发编程的三大特性原子性、一致性和可见性。再就是简单的提到了我们的volatile关键字,他可以保证我们的可见性,也就是说被volatile关键字修饰的变量如果产生了变化,可以马上刷到主存当中去。我们接下来看一下我们这次博客的内容吧。
线程:
何为线程呢?这也是我们面试当中经常问到的。按照官方的说法是:现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作 系统就会创建一个Java进程。现代操作系统调度CPU的最小单元是线程。比如我们启动QQ,就是我们启动了一个进程,我们发起了QQ语音,这个动作就是一个线程。
在这里多提一句的就是线程分为内核级线程和用户级线程,我们在java虚拟机内的线程一般都为用户级线程,也就是由我们的jvm虚拟机来调用我们的CPU来申请时间片来完成我们的线程操作的。而我们的内核级线程是由我们的系统来调度CPU来完成的,为了保证安全性,一般的线程都是由虚拟机来控制的。
上下文切换:
上面我们说过,线程是由我们的虚拟机去CPU来申请时间片来完成我们的操作的,但是不一定马上执行完成,这时就产生了上下文切换。大致就是这样的:
线程A没有运行完成,但是时间片已经结束了,我们需要挂起我们的线程A,CPU该去执行线程B了,运行完线程B,才能继续运行我们的线程A,这时就涉及到一个上下文的切换,我们把这个暂时挂起到再次运行的过程,可以理解为上下文切换(最简单的理解方式)。
可见性:
用volatile关键字修饰过的变量,可以保证可见性,也就是volatile变量被修改了,会立即刷到主内存内,让其他线程感知到变量已经修改,我们来看一个事例
public class VolatileVisibilitySample {
private volatile boolean initFlag = false; public void refresh(){
this.initFlag = true;
String threadname = Thread.currentThread().getName();
System.out.println("线程:"+threadname+":修改共享变量initFlag");
} public void load(){
String threadname = Thread.currentThread().getName();
int i = 0;
while (!initFlag){ }
System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i);
} public static void main(String[] args){
VolatileVisibilitySample sample = new VolatileVisibilitySample();
Thread threadA = new Thread(()->{
sample.refresh();
},"threadA"); Thread threadB = new Thread(()->{
sample.load();
},"threadB"); threadB.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
} }
我们想创建一个全局的由volatile修饰的boolean变量,refresh方法是修改我们的全局变量,load方法是无限循环去检查我们全局volatile修饰过的变量,我们开启两个线程,开始运行,我们会看到如下结果。
也就是说,我们的变量被修改以后,我们的另外一个线程会感知到我们的变量已经发生了改变,也就是我们的可行性,立即刷回主内存。
有序性:
说到有序性,不得不提到几个知识点,指令重排,as-if-serial语义和happens-before 原则。
指令重排:java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
指令重排一般发生在class翻译为字节码文件和字节码文件被CPU执行这两个阶段。
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因 为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被 编译器和处理器重排序。
happens-before 原则内容如下
1. 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
2. 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
3. volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
4. 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
5. 传递性A先于B ,B先于C,那么A必然先于C
6. 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
7. 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
8. 对象终结规则 对象的构造函数执行,结束先于finalize()方法。
上一段代码看看指令重排的问题。
public class VolatileReOrderSample {
private static int x = 0, y = 0;
private static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException {
int i = 0; for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread t1 = new Thread(new Runnable() {
public void run() {
a = 1;
x = b;
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join(); String result = "第" + i + "次 (" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
} }
}
我们来分析一下上面的代码
情况1:假设我们的线程1开始执行,线程2还没开始,这时a = 1 ,x = b = 0,因为b的初始值是0,然后开始执行线程2,b = 1,y = a = 1,得到结论x = 0 ,y = 1.
情况2:假设线程1开始执行,将a赋值为1,开始执行线程2,b赋值为1,并且y = a = 1,这时继续运行线程1,x = b = 1,得到结论 x = 1,y = 1.
情况3:线程2优先执行,这时b = 1,y = a = 0,然后运行线程1,a = 1,x = b = 1,得到结论 x = 1,y = 0。
不管怎么谁先谁后,我们都是只有这三种答案,不会产生x = 0且y = 0的情况,我们在下面写出来了x = 0 且 y = 0 跳出循环。我们来测试一下。
运行到第72874次结果了0,0的情况产生了,也就是说,我们t1中的a = 1;x = b;和t2中的b = 1;y = a;代码发生了改变,只有变为
Thread t1 = new Thread(new Runnable() {
public void run() { x = b;
a = 1;
}
});
Thread t2 = new Thread(new Runnable() {
public void run() { y = a;
b = 1;
}
});
这种情况才可以产生0,0的情况,我们可以把代码改为
private static volatile int a = 0, b = 0;
继续来测试,我们发现无论我们运行多久都不会发生我们的指令重排现象,也就是说我们volatile关键字可以保证我们的有序性
至少我这里570万次还没有发生0,0的情况。
就是我上次博客给予的表格
Required barriers | 2nd operation | |||
1st operation | Normal Load | Normal Store | Volatile Load | Volatile Store |
Normal Load | LoadStore | |||
Normal Store | StoreStore | |||
Volatile Load | LoadLoad | LoadStore | LoadLoad | LoadStore |
Volatile Store | StoreLoad | StoreStore |
我们来分析一下代码
线程1的。
public void run() {
a = 1;
x = b;
}
a = 1;是将a这个变量赋值为1,因为a被volatile修饰过了,我们成为volatile写,就是对应表格的Volatile Store,接下来我们来看第二步,x = b,字面意思是将b的值赋值给x,但是这步操作不是一个原子操作,其中包含了两个步骤,先取得变量b,被volatile修饰过,就成为volatile load,然后将b的值赋给x,x没有被volatile修饰,成为普通写。也就是说,这两行代码做了三个动作,分别是Volatile Store,volatile load和Store写读写,查表格我们看到volatile修饰的变量Volatile Store,volatile load之间是给予了StoreLoad这样的屏障,是不允许指令重排的,所以达到了有序性的目的。
扩展:
我们再来看一个方法,不用volatile修饰也可以防止指令重排,因为上面我们说过,volatile可以保证有序性,就是增加内存屏障,防止了指令重排,我们可以采用手动加屏障的方式也可以阻止指令重排。我们来看一下事例。
public class VolatileReOrderSample {
private static int x = 0, y = 0;
private static int a = 0, b =0; public static void main(String[] args) throws InterruptedException {
int i = 0; for (;;){
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread t1 = new Thread(new Runnable() {
public void run() {
a = 1;
UnsafeInstance.reflectGetUnsafe().storeFence();
x = b;
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
b = 1;
UnsafeInstance.reflectGetUnsafe().storeFence();
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join(); String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
} } }
storeFence就是一个有java底层来提供的内存屏障,有兴趣的可以自己去看一下unsafe类,一共有三个屏障
UnsafeInstance.reflectGetUnsafe().storeFence();//写屏障
UnsafeInstance.reflectGetUnsafe().loadFence();//读屏障
UnsafeInstance.reflectGetUnsafe().fullFence();//读写屏障
通过unsafe的反射来调用,涉及安全问题,jvm是不允许直接调用的。手写单例模式时在超高并发记得加volatile修饰,不然产生指令重排,会造成空对象的行为。后面我会科普这个玩意。
最进弄了一个公众号,小菜技术,欢迎大家的加入
java架构之路(多线程)JMM和volatile关键字(二)的更多相关文章
- 全面理解Java内存模型(JMM)及volatile关键字(转载)
关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...
- 全面理解Java内存模型(JMM)及volatile关键字(转)
原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...
- 深入理解Java内存模型JMM与volatile关键字
深入理解Java内存模型JMM与volatile关键字 多核并发缓存架构 Java内存模型 Java线程内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的,屏蔽 ...
- [转帖]java架构之路-(面试篇)JVM虚拟机面试大全
java架构之路-(面试篇)JVM虚拟机面试大全 https://www.cnblogs.com/cxiaocai/p/11634918.html 下文连接比较多啊,都是我过整理的博客,很多答案都 ...
- Java面试官最常问的volatile关键字
在Java相关的职位面试中,很多Java面试官都喜欢考察应聘者对Java并发的了解程度,以volatile关键字为切入点,往往会问到底,Java内存模型(JMM)和Java并发编程的一些特点都会被牵扯 ...
- Java面试官最爱问的volatile关键字
在Java的面试当中,面试官最爱问的就是volatile关键字相关的问题.经过多次面试之后,你是否思考过,为什么他们那么爱问volatile关键字相关的问题?而对于你,如果作为面试官,是否也会考虑采用 ...
- java架构之路(多线程)JMM和volatile关键字
说到JMM大家一定很陌生,被我们所熟知的一定是jvm虚拟机,而我们今天讲的JMM和JVM虚拟机没有半毛钱关系,千万不要把JMM的任何事情联想到JVM,把JMM当做一个完全新的事物去理解和认识. 我们先 ...
- java架构之路(多线程)大厂方式手写单例模式
上期回顾: 上次博客我们说了我们的volatile关键字,我们知道volatile可以保证我们变量被修改马上刷回主存,并且可以有效的防止指令重排序,思想就是加了我们的内存屏障,再后面的多线程博客里还有 ...
- 全面理解Java内存模型(JMM)及volatile关键字
[版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...
随机推荐
- java Jre和Jdk的区别?
JRE:(Java Runtime Environment),java运行环境.包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需的核心类库等,如果想要运行一个开 ...
- H3C Basic NAT配置示例
- 2018-8-10-win10-uwp-win2d-使用-Path-绘制界面
title author date CreateTime categories win10 uwp win2d 使用 Path 绘制界面 lindexi 2018-08-10 19:17:19 +08 ...
- 【2016常州一中夏令营Day5】
小 W 拼图[问题描述]小 W 和小 M 一起玩拼图游戏啦~小 M 给小 M 一张 N 个点的图,有 M 条可选无向边,每条边有一个甜蜜值,小 W 要选K 条边,使得任意两点间最多有一条路径,并且选择 ...
- Consul etcd ZooKeeper euerka 对比
这里就平时经常用到的服务发现的产品进行下特性的对比,首先看下结论: Feature Consul zookeeper etcd euerka 服务健康检查 服务状态,内存,硬盘等 (弱)长连接,kee ...
- k8s的网络方案对比
如下图,三台虚拟机k8s-master.k8s-node-1.k8s-node-2组成k8s集群,网络拓扑和节点IP分配如下图: 一.flannel组网方案 https://github.com/co ...
- 学习Java第四周
复习数组,数组和方法在内存中是怎样存储这个问题,有些一知半解. 复习了面向对象思想,是面向对象思想,类的定义,对象实例化,构造函数,还有用javabean的格式定义类.面向过程是自己解决问题,面向对象 ...
- 如何在ClickOnce 应用中使用 GitVersion
https://github.com/GitTools/GitVersion/issues/1153 I'm using GitVersion in an internal ClickOnce app ...
- Memcahced 缓存过期时间问题
转载:https://help.aliyun.com/knowledge_detail/38654.html 关于设置缓存数据的过期时间,可以参考以下Memcached官方说明: An expirat ...
- 2020年. NET Core面试题
第1题,什么是ASP net core? 首先ASP net core不是 asp net的升级版本.它遵循了dot net的标准架构, 可以运行于多个操作系统上.它更快,更容易配置,更加模块化,可扩 ...