Java 并发编程(三):如何保证共享变量的可见性?
上一篇,我们谈了谈如何通过同步来保证共享变量的原子性(一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行),本篇我们来谈一谈如何保证共享变量的可见性(多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值)。
我们使用同步的目的不仅是,不希望某个线程在使用对象状态时,另外一个线程在修改状态,这样容易造成混乱;我们还希望某个线程修改了对象状态后,其他线程能够看到修改后的状态——这就涉及到了一个新的名词:内存(可省略)可见性。
要了解可见性,我们得先来了解一下 Java 内存模型。
Java 内存模型(Java Memory Model,简称 JMM)描述了 Java 程序中各种变量(线程之间的共享变量)的访问规则,以及在 JVM 中将变量存储到内存→从内存中读取变量的底层细节。
要知道,所有的变量都是存储在主内存中的,每个线程会有自己独立的工作内存,里面保存了该线程使用到的变量副本(主内存中变量的一个拷贝)。见下图。

也就是说,线程 1 对共享变量 chenmo 的修改要想被线程 2 及时看到,必须要经过 2 个步骤:
1、把工作内存 1 中更新过的共享变量刷新到主内存中。
2、将主内存中最新的共享变量的值更新到工作内存 2 中。
那假如共享变量没有及时被其他线程看到的话,会发生什么问题呢?
public class Wanger {
private static boolean chenmo = false;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (!chenmo) {
}
}
});
thread.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
chenmo = true;
}
}
这段代码的本意是:在主线程中创建子线程,然后启动它,当主线程休眠 500 毫秒后,把共享变量 chenmo 的值修改为 true 的时候,子线程中的 while 循环停下来。但运行这段代码后,程序似乎进入了死循环,过了 N 个 500 毫秒,也没有要停下来的意思。
为什么会这样呢?
因为主线程对共享变量 chenmo 的修改没有及时通知到子线程(子线程在运行的时候,会将 chenmo 变量的值拷贝一份放在自己的工作内存当中),当主线程更改了 chenmo 变量的值之后,但是还没来得及写入到主存当中,那么子线程此时就不知道主线程对 chenmo 变量的更改,因此还会一直循环下去。
换句话说,就是:普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主内存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
那怎么解决这个问题呢?
使用 volatile 关键字修饰共享变量 chenmo。
因为 volatile 变量被线程访问时,会强迫线程从主内存中重读变量的值,而当变量被线程修改时,又会强迫线程将最近的值刷新到主内存当中。这样的话,线程在任何时候总能看到变量的最新值。
我们来使用 volatile 修饰一下共享变量 chenmo。
private static volatile boolean chenmo = false;
再次运行代码后,程序在一瞬间就结束了,500 毫秒毕竟很短啊。在主线程(main 方法)将 chenmo 修改为 true 后,chenmo 变量的值立即写入到了主内存当中;同时,导致子线程的工作内存中缓存变量 chenmo 的副本失效了;当子线程读取 chenmo 变量时,发现自己的缓存副本无效了,就会去主内存读取最新的值(由 false 变为 true 了),于是 while 循环也就停止了。
也就是说,在某种场景下,我们可以使用 volatile 关键字来安全地共享变量。这种场景之一就是:状态真正独立于程序内地其他内容,比如一个布尔状态标志(从 false 到 true,也可以再转换到 false),用于指示发生了一个重要的一次性事件。
至于 volatile 的原理和实现机制,本篇不再深入展开了(小编自己没搞懂,尴尬而不失礼貌的笑一笑)。
需要再次强调地是:
volatile 变量可以被看作是一种 “程度较轻的 synchronized”;与 synchronized 相比,volatile 变量运行时地开销比较少,但是它所能实现的功能也仅是 synchronized 的一部分(只能确保可见性,不能确保原子性)。
原子性我们上一篇已经讨论过了,增量操作(i++)看上去像一个单独操作,但实际上它是一个由“读取-修改-写入”组成的序列操作,因此 volatile 并不能为其提供必须的原子特性。
除了 volatile 和 synchronized,Lock 也能够保证可见性,它能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。关于 Lock 的更多细节,我们后面再进行讨论。
好了,共享变量的可见性就先介绍到这。希望本篇文章能够对大家有所帮助,谢谢大家的阅读。
上一篇:如何保证共享变量的原子性?
下一篇:如何保证对象的线程安全性
Java 并发编程(三):如何保证共享变量的可见性?的更多相关文章
- Java并发编程三个性质:原子性、可见性、有序性
并发编程 并发程序要正确地执行,必须要保证其具备原子性.可见性以及有序性:只要有一个没有被保证,就有可能会导致程序运行不正确 线程不安全在编译.测试甚至上线使用时,并不一定能发现,因为受到当时的 ...
- 【Java并发编程三】闭锁
1.什么是闭锁? 闭锁(latch)是一种Synchronizer(Synchronizer:是一个对象,它根据本身的状态调节线程的控制流.常见类型的Synchronizer包括信号量.关卡和闭锁). ...
- JAVA 并发编程-线程范围内共享变量(五)
线程范围内共享变量要实现的效果为: 多个对象间共享同一线程内的变量 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsi ...
- Java并发编程 (三) 项目准备
个人博客网:https://wushaopei.github.io/ (你想要这里多有) 一.案例环境初始化 1.环境搭建与准备 Spring Boot 项目,https://start.spr ...
- Java并发编程(三):ReentrantLock
ReentrantLock是可以用来代替synchronized的.ReentrantLock比synchronized更加灵活,功能上面更加丰富,性能方面自synchronized优化后两者性能没有 ...
- Java 并发编程(四):如何保证对象的线程安全性
01.前言 先让我吐一句肺腑之言吧,不说出来会憋出内伤的.<Java 并发编程实战>这本书太特么枯燥了,尽管它被奉为并发编程当中的经典之作,但我还是忍不住.因为第四章"对象的组合 ...
- Java 并发编程(二):如何保证共享变量的原子性?
线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心 ...
- 《Java并发编程实战》第三章 对象的共享 读书笔记
一.可见性 什么是可见性? Java线程安全须要防止某个线程正在使用对象状态而还有一个线程在同一时候改动该状态,并且须要确保当一个线程改动了对象的状态后,其它线程能够看到发生的状态变化. 后者就是可见 ...
- [Java并发编程(三)] Java volatile 关键字介绍
[Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...
随机推荐
- [kuangbin带你飞]专题一 简单搜索 Find a way HDU - 2612
Pass a year learning in Hangzhou, yifenfei arrival hometown Ningbo at finally. Leave Ningbo one year ...
- 271.已正确安装证书,但https显示连接不安全(此页面的部分内容不安全)
1.问题描述 成功安装证书,但是显示连接不安全 此页面的部分内容(例如图像)不安全 如下图 2.问题原因 页面引用(含有)http资源的文件.图片.脚本 如:图片引自其他http资源的网站 我的是引自 ...
- java中多线程执行时,为何调用的是start()方法而不是run()方法
Thead类中start()方法和run()方法的区别 1,start()用来启动一个线程,当调用start()方法时,系统才会开启一个线程,通过Thead类中start()方法来启动的线程处于就绪状 ...
- IDEA导入Maven工程
1. 2.选择要导入的工程 3. 4. 5. 6.OK之后点击4图页面的next 7.点击Finish之后 完成!
- [中秋]宇宙第一 IDE Visual Studio 了解一下
官网 https://visualstudio.microsoft.com/zh-hans/vs/ 文档 https://docs.microsoft.com/zh-cn/visualstudio/d ...
- android 多行 RadioButton的使用
最近项目用到了多行RadioButton,随记录下. 先给出RadioButton的布局 <com.kuibu.jucai.widget.MyRadioGroup android:id=&quo ...
- .Net基础篇_学习笔记_第五天_流程控制while循环003
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- java静态代码块/静态属性、构造块、构造方法执行、main方法、普通代码块的顺序
java静态代码块/静态属性.构造块.构造方法执行.main方法.普通代码块的顺序 这也是在笔试中的一个重要的考点,就有一个输出语句让你写出输出的结果. 理论知识: 静态代码块是:属于类的,在类加载时 ...
- Redis常用命令(key、string、List)
1.Key 1.keys * 查询所有数据 2.exists key名 判断key名是否存在 3.move key名 数据库号(0-15) 移动数据key名到相应的数据库 4.expire ...
- HIve实战分析Hadoop的日志
1.日志格式分析首先分析 Hadoop 的日志格式, 日志是一行一条, 日志格式可以依次描述为:日期.时间.级别.相关类和提示信息.如下所示: -03-06 15:23:48,132 INFO org ...