貌似两个多月没写博客,不知道年前这段时间都去忙了什么。

  好久以前写过一次和volatile相关的博客,感觉没写的那么深入吧,这次我们继续说我们的volatile关键字。

复习:

  先来简单的复习一遍以前写过的东西,上次我们说了内存一致性协议M(修改)E(独占)S(共享)I(失效)四种状态,还有我们并发编程的三大特性原子性、一致性和可见性。再就是简单的提到了我们的volatile关键字,他可以保证我们的可见性,也就是说被volatile关键字修饰的变量如果产生了变化,可以马上刷到主存当中去。我们接下来看一下我们这次博客的内容吧。

线程:

  何为线程呢?这也是我们面试当中经常问到的。按照官方的说法是:现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作 系统就会创建一个Java进程。现代操作系统调度CPU的最小单元是线程。比如我们启动QQ,就是我们启动了一个进程,我们发起了QQ语音,这个动作就是一个线程。

  在这里多提一句的就是线程分为内核级线程和用户级线程,我们在java虚拟机内的线程一般都为用户级线程,也就是由我们的jvm虚拟机来调用我们的CPU来申请时间片来完成我们的线程操作的。而我们的内核级线程是由我们的系统来调度CPU来完成的,为了保证安全性,一般的线程都是由虚拟机来控制的。

  用户线程:指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。另外,用户线程是由应用进程利用线程库创建和管理,不依赖于操作系统核心。不需要用户态/核心态切换,速度快。操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
  内核线程: 线程的所有管理操作都是由操作系统内核完成的。内核保存线程的状态和上下文信息,当一个线程执行了引起阻塞的系统调用时,内核可以调度该进程的其他线程执行。在多处理器系统上,内核可以分派属于同一进程的多个线程在多个处理器上运行,提高进程执行的并行度。由于需要内核完成线程的创建、调度和管理,所以和用户级线程相比这些操作要慢得多,但是仍然比进程的创建和管理操作要快。大多数市场上的操作系统,如Windows,Linux等都支持内核级线程。
  用户级线程就是我们常说的ULT,内核级线程就是我们说的KLT。线程从用户态切换到内核态时会消耗很大的性能和时间,后面说sychronized锁的膨胀升级会说到这个过程。

上下文切换:

  上面我们说过,线程是由我们的虚拟机去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关键字(二)的更多相关文章

  1. 全面理解Java内存模型(JMM)及volatile关键字(转载)

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...

  2. 全面理解Java内存模型(JMM)及volatile关键字(转)

    原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...

  3. 深入理解Java内存模型JMM与volatile关键字

    深入理解Java内存模型JMM与volatile关键字 多核并发缓存架构 Java内存模型 Java线程内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的,屏蔽 ...

  4. [转帖]java架构之路-(面试篇)JVM虚拟机面试大全

    java架构之路-(面试篇)JVM虚拟机面试大全 https://www.cnblogs.com/cxiaocai/p/11634918.html   下文连接比较多啊,都是我过整理的博客,很多答案都 ...

  5. Java面试官最常问的volatile关键字

    在Java相关的职位面试中,很多Java面试官都喜欢考察应聘者对Java并发的了解程度,以volatile关键字为切入点,往往会问到底,Java内存模型(JMM)和Java并发编程的一些特点都会被牵扯 ...

  6. Java面试官最爱问的volatile关键字

    在Java的面试当中,面试官最爱问的就是volatile关键字相关的问题.经过多次面试之后,你是否思考过,为什么他们那么爱问volatile关键字相关的问题?而对于你,如果作为面试官,是否也会考虑采用 ...

  7. java架构之路(多线程)JMM和volatile关键字

    说到JMM大家一定很陌生,被我们所熟知的一定是jvm虚拟机,而我们今天讲的JMM和JVM虚拟机没有半毛钱关系,千万不要把JMM的任何事情联想到JVM,把JMM当做一个完全新的事物去理解和认识. 我们先 ...

  8. java架构之路(多线程)大厂方式手写单例模式

    上期回顾: 上次博客我们说了我们的volatile关键字,我们知道volatile可以保证我们变量被修改马上刷回主存,并且可以有效的防止指令重排序,思想就是加了我们的内存屏障,再后面的多线程博客里还有 ...

  9. 全面理解Java内存模型(JMM)及volatile关键字

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...

随机推荐

  1. java Jre和Jdk的区别?

    JRE:(Java Runtime Environment),java运行环境.包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需的核心类库等,如果想要运行一个开 ...

  2. H3C Basic NAT配置示例

  3. 2018-8-10-win10-uwp-win2d-使用-Path-绘制界面

    title author date CreateTime categories win10 uwp win2d 使用 Path 绘制界面 lindexi 2018-08-10 19:17:19 +08 ...

  4. 【2016常州一中夏令营Day5】

    小 W 拼图[问题描述]小 W 和小 M 一起玩拼图游戏啦~小 M 给小 M 一张 N 个点的图,有 M 条可选无向边,每条边有一个甜蜜值,小 W 要选K 条边,使得任意两点间最多有一条路径,并且选择 ...

  5. Consul etcd ZooKeeper euerka 对比

    这里就平时经常用到的服务发现的产品进行下特性的对比,首先看下结论: Feature Consul zookeeper etcd euerka 服务健康检查 服务状态,内存,硬盘等 (弱)长连接,kee ...

  6. k8s的网络方案对比

    如下图,三台虚拟机k8s-master.k8s-node-1.k8s-node-2组成k8s集群,网络拓扑和节点IP分配如下图: 一.flannel组网方案 https://github.com/co ...

  7. 学习Java第四周

    复习数组,数组和方法在内存中是怎样存储这个问题,有些一知半解. 复习了面向对象思想,是面向对象思想,类的定义,对象实例化,构造函数,还有用javabean的格式定义类.面向过程是自己解决问题,面向对象 ...

  8. 如何在ClickOnce 应用中使用 GitVersion

    https://github.com/GitTools/GitVersion/issues/1153 I'm using GitVersion in an internal ClickOnce app ...

  9. Memcahced 缓存过期时间问题

    转载:https://help.aliyun.com/knowledge_detail/38654.html 关于设置缓存数据的过期时间,可以参考以下Memcached官方说明: An expirat ...

  10. 2020年. NET Core面试题

    第1题,什么是ASP net core? 首先ASP net core不是 asp net的升级版本.它遵循了dot net的标准架构, 可以运行于多个操作系统上.它更快,更容易配置,更加模块化,可扩 ...