Java 并发编程(三):如何保证共享变量的可见性?

简介: Java 并发编程(三):如何保证共享变量的可见性?

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


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


要了解可见性,我们得先来了解一下 Java 内存模型。


Java 内存模型(Java Memory Model,简称 JMM)描述了 Java 程序中各种变量(线程之间的共享变量)的访问规则,以及在 JVM 中将变量存储到内存→从内存中读取变量的底层细节。


要知道,所有的变量都是存储在主内存中的,每个线程会有自己独立的工作内存,里面保存了该线程使用到的变量副本(主内存中变量的一个拷贝)。见下图。

image.png



也就是说,线程 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;

1

再次运行代码后,程序在一瞬间就结束了,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 的更多细节,我们后面再进行讨论。


好了,共享变量的可见性就先介绍到这。希望本篇文章能够对大家有所帮助,谢谢大家的阅读。


相关文章
|
5天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
18 2
|
8天前
|
Java 开发者
深入探索Java中的并发编程
本文将带你领略Java并发编程的奥秘,揭示其背后的原理与实践。通过深入浅出的解释和实例,我们将探讨Java内存模型、线程间通信以及常见并发工具的使用方法。无论是初学者还是有一定经验的开发者,都能从中获得启发和实用的技巧。让我们一起开启这场并发编程的奇妙之旅吧!
|
9天前
|
算法 安全 Java
JAVA并发编程系列(12)ThreadLocal就是这么简单|建议收藏
很多人都以为TreadLocal很难很深奥,尤其被问到ThreadLocal数据结构、以及如何发生的内存泄漏问题,候选人容易谈虎色变。 日常大家用这个的很少,甚至很多近10年资深研发人员,都没有用过ThreadLocal。本文由浅入深、并且才有通俗易懂方式全面分析ThreadLocal的应用场景、数据结构、内存泄漏问题。降低大家学习啃骨头的心理压力,希望可以帮助大家彻底掌握并应用这个核心技术到工作当中。
|
9天前
|
Java 程序员 编译器
死磕-高效的Java编程(二)
死磕-高效的Java编程(二)
|
4天前
|
Java
JAVA并发编程系列(13)Future、FutureTask异步小王子
本文详细解析了Future及其相关类FutureTask的工作原理与应用场景。首先介绍了Future的基本概念和接口方法,强调其异步计算特性。接着通过FutureTask实现了一个模拟外卖订单处理的示例,展示了如何并发查询外卖信息并汇总结果。最后深入分析了FutureTask的源码,包括其内部状态转换机制及关键方法的实现原理。通过本文,读者可以全面理解Future在并发编程中的作用及其实现细节。
|
7天前
|
Java 数据处理 调度
Java中的多线程编程:从基础到实践
本文深入探讨了Java中多线程编程的基本概念、实现方式及其在实际项目中的应用。首先,我们将了解什么是线程以及为何需要多线程编程。接着,文章将详细介绍如何在Java中创建和管理线程,包括继承Thread类、实现Runnable接口以及使用Executor框架等方法。此外,我们还将讨论线程同步和通信的问题,如互斥锁、信号量、条件变量等。最后,通过具体的示例展示了如何在实际项目中有效地利用多线程提高程序的性能和响应能力。
|
8天前
|
安全 算法 Java
Java中的多线程编程:从基础到高级应用
本文深入探讨了Java中的多线程编程,从最基础的概念入手,逐步引导读者了解并掌握多线程开发的核心技术。无论是初学者还是有一定经验的开发者,都能从中获益。通过实例和代码示例,本文详细讲解了线程的创建与管理、同步与锁机制、线程间通信以及高级并发工具等主题。此外,还讨论了多线程编程中常见的问题及其解决方案,帮助读者编写出高效、安全的多线程应用程序。
|
2月前
|
存储 Java
02 Java基础语法(变量+数据类型+运算符)(下)
02 Java基础语法(变量+数据类型+运算符)
22 5
|
4月前
|
存储 Java
Java基础手册(标识符 关键字 字面值 变量 数据类型 字符编码 运算符 控制语句 方法及方法重载和递归 面向对象与面向过程)
Java基础手册(标识符 关键字 字面值 变量 数据类型 字符编码 运算符 控制语句 方法及方法重载和递归 面向对象与面向过程)
31 0
|
5月前
|
存储 安全 Java
Java中的变量与数据类型——基本数据类型与引用数据类型深入剖析
Java中的变量与数据类型——基本数据类型与引用数据类型深入剖析
73 1
下一篇
无影云桌面