上一篇,我们谈了谈如何通过同步来保证共享变量的原子性(一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行),本篇我们来谈一谈如何保证共享变量的可见性(多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值)。

我们使用同步的目的不仅是,不希望某个线程在使用对象状态时,另外一个线程在修改状态,这样容易造成混乱;我们还希望某个线程修改了对象状态后,其他线程能够看到修改后的状态——这就涉及到了一个新的名词:内存(可省略)可见性。

要了解可见性,我们得先来了解一下 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 并发编程(三):如何保证共享变量的可见性?的更多相关文章

  1. Java并发编程三个性质:原子性、可见性、有序性

      并发编程 并发程序要正确地执行,必须要保证其具备原子性.可见性以及有序性:只要有一个没有被保证,就有可能会导致程序运行不正确  线程不安全在编译.测试甚至上线使用时,并不一定能发现,因为受到当时的 ...

  2. 【Java并发编程三】闭锁

    1.什么是闭锁? 闭锁(latch)是一种Synchronizer(Synchronizer:是一个对象,它根据本身的状态调节线程的控制流.常见类型的Synchronizer包括信号量.关卡和闭锁). ...

  3. JAVA 并发编程-线程范围内共享变量(五)

    线程范围内共享变量要实现的效果为: 多个对象间共享同一线程内的变量 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsi ...

  4. Java并发编程 (三) 项目准备

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.案例环境初始化 1.环境搭建与准备 Spring Boot 项目,https://start.spr ...

  5. Java并发编程(三):ReentrantLock

    ReentrantLock是可以用来代替synchronized的.ReentrantLock比synchronized更加灵活,功能上面更加丰富,性能方面自synchronized优化后两者性能没有 ...

  6. Java 并发编程(四):如何保证对象的线程安全性

    01.前言 先让我吐一句肺腑之言吧,不说出来会憋出内伤的.<Java 并发编程实战>这本书太特么枯燥了,尽管它被奉为并发编程当中的经典之作,但我还是忍不住.因为第四章"对象的组合 ...

  7. Java 并发编程(二):如何保证共享变量的原子性?

    线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心 ...

  8. 《Java并发编程实战》第三章 对象的共享 读书笔记

    一.可见性 什么是可见性? Java线程安全须要防止某个线程正在使用对象状态而还有一个线程在同一时候改动该状态,并且须要确保当一个线程改动了对象的状态后,其它线程能够看到发生的状态变化. 后者就是可见 ...

  9. [Java并发编程(三)] Java volatile 关键字介绍

    [Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...

随机推荐

  1. Features Track 2018徐州icpc网络赛 思维

    Morgana is learning computer vision, and he likes cats, too. One day he wants to find the cat moveme ...

  2. 面向微服务的体系结构评审中需要问的三个问题-咖啡杂谈:Java、新闻、故事和观点

    面向微服务的体系结构如今风靡全球.这是因为更快的部署节奏和更低的成本是面向微服务的体系结构的基本承诺. 然而,对于大多数试水的公司来说,开发活动更多的是将现有的单块应用程序转换为面向微服务的体系结构, ...

  3. protobuf初识

    protobuf是一种高效的数据格式,平台无关.语言无关.可扩展,可用于 RPC 系统和持续数据存储系统. protobuf protobuf介绍 Protobuf是Protocol Buffer的简 ...

  4. 一个例子明白 javascript 中 for 与 for in 的区别

    var arr = new Array(); arr["a"] = "aa"; arr["b"] = "bb"; arr ...

  5. 基础知识:Mysql基本操作命令

    启动mysql :  mysql -hlocalhost -uroot -p 创建数据库:create database 数据库名字; 指定要操作的数据库:use 数据库名字; 查看数据表建表语句:s ...

  6. (1)jsoncpp库的使用

        本节主要介绍 json是什么以及jsoncpp库的使用. (1)JSON是什么 json 是一种轻量级的文本数据交换格式: json 独立于语言.平台,使用java script语法来描述对象 ...

  7. System类入门学习

    System类 System类代表系统,系统级的很多属性和控制方法都放置在该类的内部.该类位于java.lang包.其包含一些有用的字段和方法,不能被实例化 system中包含了in.out和err三 ...

  8. Linux下Eclipse以及Java环境安装教程[小白化](2019-9)

    Linux下安装Eclipse以及Java 一.前言 许久未用Eclipse, Ubuntu上也没装Eclipse, 今天安装发现, 好多东西都忘了. 不过经过一番查找(百度, csdn) 终于还是安 ...

  9. charles 反向代理

    本文参考:charles 反向代理 这个比较有逼格了: 正向代理和反向代理的区别: 正向代理:是代理客户端,为客户端收发请求,使真实客户端对服务器不可见:在客户这一端的,替客户收发请求(类似现在正常使 ...

  10. 1小时让你掌握响应式编程,并入门Reactor

    我看同步阻塞 “你知道什么是同步阻塞吗”,当然知道了.“那你怎么看它呢”,这个... 在同步阻塞的世界里,代码执行到哪里,数据就跟到哪里.如果数据很慢跟不上来,代码就停在那里等待数据的到来,然后再带着 ...