前言

  上一章节简单介绍了线程安全以及最基础的保证线程安全的方法,建议大家手敲代码去体会。这一章会提到volatile关键字,虽然看起来很简单,但是想彻底搞清楚需要具备JMM、CPU缓存模型的知识。不要小看这个关键字,它在整个并发包(concurrent包)使用的非常广泛,掌握volatile关键字是非常重要的。

  如果你是一个急性子,请看下面3点就行:

  • 保证了多线程读取变量的可见性,一个线程修改volatile修饰的变量,另外一个线程会立即读取到新的值
  • 禁止指令重排序
  • volatile关键字不会像synchronized关键字一样造成线程阻塞,也就是说无锁

1.1 初识volatile关键字

  我先写一个例子,在主线程启动2个线程,一个线程负责写,一个线程负责读,读写的该变量就是共享变量,那么结果是你想的那样吗?

/**
* volatile第一个演示Demo类。
*
* @author GrimMjx
*/
public class VolatileDemo1 { //i的初始值为0
public static int i;
//i的最大值为3
public static int MAX = 3; public static void main(String[] args) {
//读线程
new Thread(() -> {
int index = i;
while (index < MAX) {
if (i != index) {
System.out.println("i = " + i);
index = i;
}
}
}).start(); //写线程
new Thread(() -> {
int index = i;
while (index < MAX) {
System.out.println("new i = " + ++i);
index = i;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

  我贴上一份我执行的结果:

new i = 1
i = 1
new i = 2
new i = 3

  程序不会停,需要手动停。那么问题来了,为什么明明写入了i了,读线程还是无法读到新的i的值呢?读线程压根没有感知到i的变化!只要我们把变量i的定义改变一下,那么就可以解决这个问题。

public volatile static int i;

  改好之后再试一下,确实如我们预料的执行了且读线程也不会死循环。只是一个关键字的差别,会发生很大的不同。那接下来请带着疑问去学习。

new i = 1
i = 1
new i = 2
i = 2
new i = 3
i = 3

1.2 机器CPU

  所有的指令都是CPU寄存器完成的,CPU指令的过程中涉及到数据的写入和读取。CPU能访问的所有数据只能是RAM(计算机内存)。虽然CPU频率不断提升,但是RAM访问速度没有很大突破,因此CPU处理速度和内存的访问速度差距巨大,一次主内存的访问通常在几十到几百个甚至上千个时钟周期,一次L1高速缓存的读写需2个左右时钟周期,一次L2高速缓存的读写需要几十个时钟周期。

1.2.1 CPU Cache模型

  可以直观看到两边的速度严重不对等,于是有了在CPU和主内存之间增加缓存,最靠近CPU的缓存成为L1高速缓存,其次是L2,L3和主内存。我们先看一张各级缓存之间响应时间差距,以及内存到底有多慢。

  接下来我们看一下CPU Cache模型:

1.2.2 CPU缓存一致性

  缓存大大提高了访问速度,但是同时也引入了缓存不一致的问题,比如i++;这个操作。具体的过程如下:

  1. 读取主内存的i到CPU Cache中
  2. 将i+1
  3. 将结果写回CPU Cache
  4. 将数据刷新回主内存

  i++在单线程完全不会有问题,但是多线程的时候就会有问题,每个线程都有自己的工作内存(本地内存),如果在2个线程都执行i++;操作,A线程和B线程此时的工作内存中的i都是0,加1之后都变成1。最后经过计算再写入主内存可能结果还是1。这就是缓存不一致问题。如果想要解决这个问题,主流方法是通过缓存一致性协议(MESI协议)。这个协议的大致思想就是如果当CPU在操作Cache中的数据时,其他Cache也存在一份副本,那么会进行如下操作:

  1. 读取操作,不做任何处理,只是将Cache中的数据读取到寄存器
  2. 写入操作,发出信号通知其他CPU将其变量中的Cache line置为无效状态,其他CPU在进行该变量的读取时候不得不到主内存中再次获取

1.3 Java内存模型

  JMM指定了JVM和计算机RAM如何进行工作的,同时也决定了一个线程对共享变量的写入何时对其他线程可见,有以下几个要点:

  • 每个线程都有自己的工作内存,也成为本地内存
  • 工作线程只存储线程对共享变量的副本
  • 线程不能直接操作主内存,只能操作工作内存
  • 工作内存和JMM都是一个抽象的概念,实际并不存在,覆盖了寄存器,编译器优化等等

  主内存和工作内存的关系和CPU与CPUCache之间的关系是非常类似的,所以通过图示和讲解,我们发现理解volatile关键字会比synchronized关键字困难很多,需要了解机器CPU还有JMM。volatile在JDK5以后的concurrent包运用非常广泛,所以掌握volatile关键字很重要。

1.4 深入理解volatile关键字

  说到并发,有三大特性,原子性,有序性和可见性,那我们从三个方面来介绍

1.4.1 原子性

  volatile不具备原子性

  原子性的意思就是在一次操作中,所有的操作全部执行或者都不执行,就像名字一样是不可分割的。Java中,对变量的读取和赋值操作都是源自的,但是多个原子性的操作在一起,不一定是个原子操作。JMM只保证了基本的读取和赋值的原子性,其他的均不保证。说回volatile,如果在上一章节的UnsafeAdd的例子,用volatile修饰变量i,是否可以解决多线程并发问题呢,结果是不可以的,可以自己去试试。

  就像是i++操作,他其实包含了3步骤

  1.从主内存获取i,缓存到工作内存

  2.在工作内存中进行+1

  3.刷回主内存。

  这也就是刚刚说的多个原子性的操作在一起,不一定是个原子操作

  Java中想要保证原子性,需要使用synchronized关键字,concurrent包的lock,原子封装类和循环CAS的方式(原子变量是一种更好的volatile,后面会讲到)

1.4.2 可见性

  volatile具备可见性

  读取:当一个变量被volatile关键字修饰时,当其他线程对此变量进行了修改,则会迫使其他线程的工作内存中的该变量失效,所以必须从主内存重新获取。(使用的是机器指令lock)

  写入:当然是先修改工作内存,修改后立即将其刷新到主内存中。

  Java中volatile,synchronized关键字和显式锁lock都保证可见性

1.4.3 有序性

  volatile具备有序性

  首先volatile遵循happens-before原则:对一个变量的写操作要早于这个变量之后的读操作。也就是说,如果一个变量使用volatile关键字修饰,一个线程对这个变量进行写操作,另外一个线程对这个变量进行读操作。那么写操作肯定要先发生于读操作。

  volatile对顺序性非常霸道,直接禁止JVM和处理器进行指令重排序,但是对于volatile前后无依赖关系的执行可以随便排序。

  Java中volatile,synchronized关键字和显式锁lock都保证有序性

1.5 volatile的正确打开姿势

  • 确保它们所引用状态的可见性
  • 标识一些重要的程序生命周期事件发生(init,destroy)
  • 确保只有一个线程更新变量的值
  • 不会用就不要用:)

Java并发专题(三)深入理解volatile关键字的更多相关文章

  1. Java并发编程(六)volatile关键字解析

    由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识. 一.内存模型的相关概念 Java内存模型规定所有的变量都是存在 ...

  2. 【Java并发编程】6、volatile关键字解析&内存模型&并发编程中三概念

    volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以 ...

  3. Java并发编程:JMM和volatile关键字

    转载请标明出处: http://blog.csdn.net/forezp/article/details/77580491 本文出自方志朋的博客 Java内存模型 随着计算机的CPU的飞速发展,CPU ...

  4. Java并发编程——为什么要用volatile关键字

    首发地址 https://blog.leapmie.com/archives/66ba646f/ 日常编程中出现 volatile 关键字的频率并不高,大家可能对 volatile 关键字比较陌生,再 ...

  5. Java并发机制(3)--volatile关键字与内存模型

    Java并发编程:volatile关键字解析及内存模型 个人整理自:博客园-海子-http://www.cnblogs.com/dolphin0520/p/3920373.html 1.线程内存模型: ...

  6. 【Java并发编程】从CPU缓存模型到JMM来理解volatile关键字

    目录 并发编程三大特性 原子性 可见性 有序性 CPU缓存模型是什么 高速缓存为何出现? 缓存一致性问题 如何解决缓存不一致 JMM内存模型是什么 JMM的规定 Java对三大特性的保证 原子性 可见 ...

  7. Java并发(3)- 聊聊Volatile

    引言 谈到volatile关键字,大多数开发者都有一定了解,可以说是开发者非常熟悉,深入之后又非常陌生的一个关键字.相当于轻量的synchronized,也叫轻量级锁,与synchronized相比性 ...

  8. 深入理解volatile关键字

    Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的. Java内存模型规定了所有的变量都存储在主内存中.每条线程中还有自己的工作内存,线程的工作内存 ...

  9. 对精致码农大佬的 [理解 volatile 关键字] 文章结论的思考和寻找真相

    一:背景 1. 讲故事 昨天在园里的编辑头条看到 精致码农大佬 写的一篇题为:[C#.NET 拾遗补漏]10:理解 volatile 关键字 (https://www.cnblogs.com/will ...

随机推荐

  1. BZOJ_1552_[Cerc2007]robotic sort_splay

    BZOJ_1552_[Cerc2007]robotic sort_splay 题意: 分析: splay维护区间操作 可以先把编号排序,给每个编号分配一个固定的点,映射过去 查找编号的排名时先找到这个 ...

  2. CountDownLatch和CyclicBarrier 区别

    CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行. CyclicBarrier        : N个线程相互等待,任何一个线程完成之前,所有的线程都 ...

  3. mySql入门-(一)

    学了很多乱七杂八的东西,但是依然停留在前端,在工作中一直和后端交流,但是不太了解数据库是怎么回事,为了加强学习,准备学习一些关于数据库相关的东西. 说起数据库可能会有很多很多,SQLServer.Or ...

  4. shiro的SecurityUtis

    接着上一篇来继续分析shiro源码 这篇主要讲解shiro里面的SecurityUtils 首先我们看该类供我们在业务中用的仅有两个get方法,那么这两个get方法获取的subject和sercuri ...

  5. .net core 中间件管道底层剖析

    .net core 管道(Pipeline)是什么? 由上图可以看出,.net core 管道是请求抵达服务器到响应结果返回的中间的一系列的处理过程,如果我们简化一下成下图来看的话,.net core ...

  6. 直观理解神经网络最后一层全连接+Softmax

    目录 写在前面 全连接层与Softmax回顾 加权角度 模板匹配 几何角度 Softmax的作用 总结 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 这篇文章将从3 ...

  7. aps .net MVC单用户登录

    当不允许多用户同时登录一个帐号时,就需要一种机制,当再登录一个相同的帐号时,前面登录的人被挤下线. 原文地址:http://www.cnblogs.com/f23wangj/p/4984302.htm ...

  8. forEach循环对集合进行循环时,需判断是否为null;

    分析forEach的源码会发现:foreach源码例子: public class Foreach { public static void main(String[] args) { List< ...

  9. Android之应用市场排行榜、上架、首发

    文章大纲 一.应用市场排行榜介绍二.应用市场上架介绍三.应用市场首发介绍四.参考文档   一.应用市场排行榜介绍   iiMedia Research(艾媒咨询)权威发布<2017-2018中国 ...

  10. 安卓开发笔记(二十):利用夜神模拟器调试运行Android Studio的apk

    一.首先来到夜神模拟器的安装目录下 如下图所示: 再把这整个文件夹添加到我们的windows环境变量里.然后再把android studio 和夜神模拟器都打开,注意必须同时打开而且不能够把夜神模拟器 ...