你真的会用volatile吗
volatile的概念volatile详解什么时候需要使用volatilevolatile在标准库里的应用volatile会降低程序执行的效率volatile不是万能的
volatile的概念
或者说,volatile解决什么问题?
我自己的总结:volatile解决多线程下变量访问的内存可见性问题,用于线程间通信。
通信怎能理解呢,线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
java语言标准规范对volatile的描述是这样的:
The Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables. The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes. A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).
上面这段话摘自这个链接,有兴趣的可以自己点开看。
https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.3.1.4
大概意思是,java语言允许多个线程访问共享变量。为了保证共享变量能准确一致的更新,线程要保证通过锁的机制单独获得这个变量。java提供了一种机制,允许你把变量定义成volatile,在某些情况下比直接用锁更加方便。
如果一个变量被定义成volatile,java内存模型确保所有线程看到的这个共享变量是一致的。
这个一致怎么理解呢?继续往下看。
volatile详解
先来看一幅图,
这是一幅计算的内存架构图。
现在的CPU大部分都是多核的,在计算机内部,变量读写的流程是这样的:
- 当一个处理器需要读取变量的时候,首先会把变量从主内存读到缓存,也有可能是寄存器,然后再做各种计算。
- 计算的结果由寄存器刷新到缓存,然后再由缓存刷新到主内存。
- 一个处理器的缓存回写到内存会导致其他处理器的缓存无效,这样其它处理器
这里的一个关键点是,什么时候刷新?答案是不知道。我们不能假设CPU什么时候会刷新。这样就会带来一些问题,比如一个线程写完一个共享变量,还没有刷新到主内存。然后另一个线程读这个变量还是旧的值,在很多场景下,这个结果和程序员期望的并不一致。
幸运的是,我们虽然不知道CPU什么时候刷新,但是我们可以强制CPU执行刷新。
再来看一个图,这是JAVA的内存模型图。
本地内存是JVM里一个抽象的概念,它可以涵盖寄存器,缓存等。
我们把这两幅图对应起来,可以这样解释。
在JAVA中,当一个线程写变量时,会先把这个变量从主内存拷贝一份线程的本地内存,然后在本地内存操作。操作完成之后,再刷新到主内存。只有刷新后,另一个线程才能读取新的值。
来看个例子:
public class VolatileTest implements Runnable {
private boolean running = true;
@Override
public void run() {
if (running) {
System.out.println("I am running");
}
}
public void stop() {
running = false;
}
}
这段代码在多线程环境下执行的时候,假设A线程正在执行run
方法,B线程执行了stop
方法,我们的程序没法保证A线程什么时候会马上停止。因为这取决于CPU什么时候进行刷新,把最新变量的值同步到主内存。
解决方法是,把running
这个共享变量用volatile修饰即可,这样可以保证B线程的修改会立刻刷新到主内存,对其它线程可见。
public class VolatileTest implements Runnable {
private volatile boolean running = true;
再来看个稍微复杂一点的例子。
public class VolatileTest {
public volatile int a = 0;
volatile boolean flag = false;
public void write() {
a = 1; // 位置1
flag = true; //// 位置2
}
public void read() {
if (flag) { // 位置3
int i = a; // 位置4
}
}
}
Java规范对于volatile变量规则是:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
假设线程A执行writer()方法之后,线程B执行reader()方法。根据volatile变量的happens-before规则,位置2必然先于位置3执行。同时我们知道在同一个线程中,所有操作必须按照程序的顺序来执行,所以位置1肯定早于位置2,位置3早于位置4。然后我们能推出位置1早于位置4。
这样的顺序是符合我们预期的。
这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之 前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。
什么时候需要使用volatile
通过上面的例子,我们可以总结下volatile的使用场景。
通常是,存在一个或者多个共享变量,会有线程对他们写操作,也会有其它线程对他们读操作。这样的变量都应该使用volatile修饰。
volatile在标准库里的应用
ConcurrentHashMap里用到了一些volatile的操作,比如:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
...
可以看到,用于存储值的value变量就是volatile类型,这样可以保证在多线程读取的时候,不会读到过期的值。之所以不会读到过期的值,是因为根据Java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。
volatile会降低程序执行的效率
不要过度使用volatile,不必要的场景没有必要用volatile修饰变量,尽管这样做程序也不会出什么错。
根据前面的描述,volatile相当于给变量的操作加了“锁”,每次操作都有加锁和释放锁的动作,效率自然会受影响。
volatile不是万能的
对volatile经常有一中误解就是,它可以保证原子操作。
通过上面的例子,我们知道,volatile关键字可以保证内存可见性,指令执行的有序性。但是请一定记住,它没法保证原子性。举个例子你可能比较容易明白。
public class VolatileTest {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final VolatileTest test = new VolatileTest();
for(int i=0;i<10;i++){
new Thread(() -> {
for(int j=0;j<1000;j++)
test.increase();
}).start();
}
while(Thread.activeCount()>2) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
执行这段代码,会发现结果每次一般都不同,但是肯定都小于10*1000。这就是volatile不保证原子性的最好证据。那么深层次的原因是什么呢?
事实上,自增操作包括三个步骤:
- 读取变量的原始值
- 进行加1操作
- 写入线程工作内存
既然分了三个步骤,就有可能出现下面这种情况:
假如某个时刻变量inc的值为10。
第一步,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
第二步, 然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2会直接去主存读取inc的值,此时inc的值时10;
第三步, 线程2进行加1操作,并把11写入工作内存,最后写入主存。
第四步,线程1接着进行加1操作,由于已经读取了inc的值,此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
最后,两个线程分别进行了一次自增操作后,但是inc只增加了1。
有很多人会在第三步和第四步那里有疑问,线程2更新inc的值以后,不是会导致线程1工作内存中的值失效吗?
答案是不会,因为在一个操作中,值只会读取一次。这个是原子性和可见性区分的核心。
解决方案是使用increase方法使用synchronized
同步锁修饰。具体不展开了。
参考:
- 《java并发编程的艺术》
- https://www.cnblogs.com/dolphin0520/p/3920373.html
- 设置输出延迟
- 设置输入延时约束
- MySQL 死锁与日志二三事
- 一千个不用 Null 的理由
- TensorFlow强化学习入门(1.5)——上下文赌博机
- 以太坊·代币开发详解
- JSON Web Token - 在Web应用间安全地传递信息
- TensorFlow强化学习入门(2)——基于策略的Agents
- 用ABAP 生成二维码 QR Code
- CDS view注解解析 - @Environment.systemField
- Document flow API in SAP CRM and C4C
- Python基础知识4:文件操作
- Python基础知识6:格式化字符、颜色
- 给自定义控件(Web Control)添加事件的几种方法。前两种方法可以不实现IPostBackEventHandler
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 【测试开发-1】基于Springboot+layui实现接口自动化平台
- 【SpringBoot-2】SLF4J+logback进行日志记录
- 【JMeter-3】JMeter参数化4种实现方式
- 【JMeter-1】JMeter安装与接口测试入门
- 【JMeter-2】JMeter接口测试之断言实现
- 【UI自动化-1】UI自动化环境搭建与简单示例
- 【UI自动化-2】UI自动化元素定位专题
- 【UI自动化-3】UI自动化元素操作专题
- maven的安装与使用
- 【Java多线程-1】线程概述与线程创建和使用
- 【Java多线程-2】Java线程池详解
- 【Java多线程-3】Future与FutureTask
- 【Java多线程-4】CompletionService详解
- 【Java多线程-5】 CompletableFuture详解
- 【Java多线程-6】synchronized同步锁