Java并发编程volatile关键字的作用

日常编程中出现 volatile 关键字的频率并不高,大家可能对 volatile 关键字比较陌生,再深入一点也许是听闻 volatile 只能保证可见性而不能保证原子性,无法有效保证线程安全,于是更加避免使用 volatile ,简简单单加上synchronize关键字就完事了。本文稍微深入探讨 volatile 关键字,分析其作用及对应的使用场景。

并发编程的几个概念简述

首先简单介绍几个与并发编程相关的概念:

  • 可见性

可见性是指变量在线程之间是否可见,JVM 中默认情况下线程之间不具备可见性。

  • 原子性

对于 a = 0 操作是属于原子操作,但 a = a + 1 则不是原子操作,因为这里涉及到要先读取原来 a 的值,然后再为 a 加 1 ,当涉及多线程同时执行该语句时,会出现值不稳定的情况,所以非原子操作在并发场景下是不安全的。

  • 有序性

java 内存模型中允许编译器和处理器进行指令重排优化,重排过程中不会影响单个线程的指令执行顺序,但会影响多线程环境中的运行正确性

  • 指令重排

在多核 CPU 的情况下,为了充分利用时间片,提高指令执行效率,处理器会根据一定规则对指令进行重排序,由于规则的限定,指令重排后理论上最终运行结果不变。

volatile 的主要作用

volatile 的主要作用是实现可见性 和禁止指令重排

  • 实现可见性

在 JVM 内存模型中内存分为主内存和工作内存,各线程有独自的工作内存,对于要操作的数据会从主内存拷贝一份到工作内存中,默认情况下工作内存是相互独立的,也就是线程之间不可见,而 volatile 最重要的作用之一就是使变量实现可见性。

  • 禁止指令重排

虽然指令重排理论上不会影响执行结果的正确性,但指令重排只能保证底层的机器语言重排序后结果正确,而对于Java高级语言,所以在没有干预的情况下并不能确保每条语句在编译对应的指令重排后与期望的执行效果一致。

对于以下示例,由于 ready 没有指定 volatile ,当变量 ready 线程间不可见时,可能导致线程中读不到 ready 的新值,无法停止循环;如果指令重排序,可能在线程执行前变量 ready 已赋值为 true ,导致线程内容不打印。

public class NoVisibility {
 private static boolean ready;
 private static int number;
 private static class ReaderThread extends Thread {
  @Override
  public void run() {
   while(!ready) {
    Thread.yield();
   }
   System.out.println("1");
  }
 }
 public static void main(String[] args) {
  new ReaderThread().start();
  ready = true;
 }
}

为什么volatile不能保证线程安全?

想要线程安全必须保证原子性,可见性,有序性,而 volatile 只能保证可见性和有序性。

volatile 字段主要是让线程从主内存中获取值从而保证可见性,但是CPU中还有一层高速缓存——寄存器,对于非原子性操作,在底层指令运算中还是会出现数据缓存导致运算结果不正确的情况,从而无法保证线程安全。
简单来说,volatile 在多 cpu 环境下不能保证其它 cpu 的缓存同步刷新,因此无法保证原子性。

为什么不直接用synchronized

synchronized 可保证原子性、可见性、有序性,能有效保证线程安全,但是有个缺点是性能开销较大,而 volatile 是轻量级的线程安全实现方案,在某些特定场合下也能保证线程安全。由于 synchronized 的便捷性,也容易导致 synchronized 的滥用。

双重检查锁

因为 volatile 不能简易的实现线程安全,需要有较深入的了解才能正确使用,所以 volatile也显得更为复杂,使用频率也较低,而 volatile 的一个典型使用例子是双重检查锁模式。

双重检查锁通常用于单例模式或延迟赋值的场景,其代码通常如下

public class Singleton {
 private volatile static Singleton uniqueSingleton; // 1. 为变量添加volatile修饰符

 private Singleton() {
 }

 public Singleton getInstance() {
  if (null == uniqueSingleton) { //2. 第一重检查
   synchronized (Singleton.class) { // 3. synchronized加锁
    if (null == uniqueSingleton) { // 4. 第二重检查
     uniqueSingleton = new Singleton();
    }
   }
  }
  return uniqueSingleton;
 }
}

以下是对这段代码的一些疑问及解答:

Q: 为什么不在 getInstance 方法直接加 synchronized ?

A: 只有在第一次初始化时才需要加锁,如果在getInstance方法上加锁则每次获取实例时都会对整段代码块加锁,影响性能

Q: 为什么需要双重检查?

A: 如果多线程同时通过了第一次检查,其中一个线程需要通过了第二次检查才进行实例化对象,其余线程在后续等待获取到锁后则判断到变量非空,跳过赋值操作。

Q: 为什么 uniqueSingleton 需要添加volatile关键字?

A: 对于 uniqueSingleton = new Singleton();语句,实际上可以分解成以下三个步骤:

  • 分配内存空间
  • 初始化对象
  • 将对象指向刚分配的内存空间

但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:

  • 分配内存空间
  • 将对象指向刚分配的内存空间
  • 初始化对象

现在考虑重排序后,两个线程发生了以下调用:

Time Thread A Thread B
T1 检查到uniqueSingleton为空
T2 获取锁
T3 再次检查到uniqueSingleton为空
T4 为uniqueSingleton分配内存空间
T5 将uniqueSingleton指向内存空间
T6 检查到uniqueSingleton不为空
T7 访问uniqueSingleton(此时对象还未完成初始化)
T8 初始化uniqueSingleton

在这里添加volatile关键字主要是避免在对象未完整完成对象创建就已经被其他线程读取,造成空指针异常。

总结

  1. volatile 的主要作用是实现可见性和禁止指令重排。
  2. 线程安全需要满足可见性、有序性、原子性。
  3. volatile 可以保证可见性和有序性,但是无法保证原子性,所以是线程不安全的。(非原子操作可能会导致数据缓存在CPU的cache中,产生数据不一致)
  4. synchronized 关键字虽然可以保证可见性、有序性、原子性,而且用法简单,但是性能开销大。
  5. 双重检查锁模式是 volatile 的典型使用场景,双重检查锁通常用于实现单例模式或延迟赋值。

以上就是Java并发编程volatile关键字的作用的详细内容,更多关于Java volatile关键字的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java中的关键字volatile详解

    volatile关键字经常用来修饰变量.不过,volatile本身很容易被误用.本篇就介绍一下volatile的原理和使用方式. 在介绍volatile关键字原理前,我们首先要了解JVM运行时的内存分配逻辑. 对于成员变量i,它存储在堆内存中.每个线程在运行时都会有一个自己的线程栈,线程如果要访问类的成员变量i,会通过引用获取到堆中变量i实际的值10,然后把这个变量值拷贝到自己的栈内存中,作为一个变量副本,之后线程便不再会与堆中的变量有实际联系.每个线程都有一个自己的本地副本,相互隔离.线程访问

  • Java并发编程:volatile关键字详细解析

    volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以重获生机. volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情.由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用vola

  • Java多线程并发编程 Volatile关键字

    volatile 关键字是一个神秘的关键字,也许在 J2EE 上的 JAVA 程序员会了解多一点,但在 Android 上的 JAVA 程序员大多不了解这个关键字.只要稍了解不当就好容易导致一些并发上的错误发生,例如好多人把 volatile 理解成变量的锁.(并不是) volatile 的特性: 具备可见性 保证不同线程对被 volatile 修饰的变量的可见性. 有一被 volatile 修饰的变量 i,在一个线程中修改了此变量 i,对于其他线程来说 i 的修改是立即可见的. 如: vola

  • 深入理解Java中的volatile关键字(总结篇)

    基本概念 -------------------------------------------------------------------------------- 先补充一下概念:Java 内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个

  • 深入了解Java中Volatile关键字

    一.基本概念 先补充一下概念:Java 内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.也就是一个线程修改的结果.另一个线程马上就能看到.比如:用volatile修饰的变量,就会具有可见性.volatile修饰

  • 详细分析java并发之volatile关键字

    Java面试中经常会涉及关于volatile的问题.本文梳理下volatile关键知识点. volatile字意为"易失性",在Java中用做修饰对象变量.它不是Java特有,在C,C++,C#等编程语言也存在,只是在其它编程语言中使用有所差异,但总体语义一致.比如使用volatile 能阻止编译器对变量的读写优化.简单说,如果一个变量被修饰为volatile,相当于告诉系统说我容易变化,编译器你不要随便优化(重排序,缓存)我. Happens-before 规范上,Java内存模型遵

  • java并发编程专题(一)----线程基础知识

    在任何的生产环境中我们都不可逃避并发这个问题,多线程作为并发问题的技术支持让我们不得不去了解.这一块知识就像一个大蛋糕一样等着我们去分享,抱着学习的心态,记录下自己对并发的认识. 1.线程的状态: 线程状态图: 1.新建状态(New):新创建了一个线程对象. 2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权. 3.运行状态(Running):就绪状态的线程获取了CPU,执行程序代码. 4

  • 一文精通Java中的volatile关键字

    前言 在一些开源的框架的源码当中时不时都可以看到volatile这个关键字,最近特意学习一下volatile关键字的使用方法. volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见. 相较于 synchronized 是一种较为轻量级的同步策略. 缺点: 1. volatile 不具备"互斥性" 2. volatile 不能保证变量的"原子性" 很多资料中是这样介绍volatile关键字的: volatile是轻量级的synchroniz

  • Java并发编程volatile关键字的作用

    日常编程中出现 volatile 关键字的频率并不高,大家可能对 volatile 关键字比较陌生,再深入一点也许是听闻 volatile 只能保证可见性而不能保证原子性,无法有效保证线程安全,于是更加避免使用 volatile ,简简单单加上synchronize关键字就完事了.本文稍微深入探讨 volatile 关键字,分析其作用及对应的使用场景. 并发编程的几个概念简述 首先简单介绍几个与并发编程相关的概念: 可见性 可见性是指变量在线程之间是否可见,JVM 中默认情况下线程之间不具备可见

  • Java并发编程——volatile关键字

    一.volatile是什么 volatile是Java并发编程中重要的一个关键字,被比喻为"轻量级的synchronized",与synchronized不同的是,volatile只能修饰变量,无法修饰方法及代码块等. 下面是使用volatile关键字实现的单例模式: public class Singleton implements Serializable { private static volatile Singleton singleton; private Singleto

  • Java并发编程之关键字volatile知识总结

    一.作用 被 volatile 修饰的变量 1.保证了不同线程对该变量操作的内存可见性 2.禁止指令重排序 二.可见性 Java 内存模型(Java Memory Model) 是 Java 虚拟机定义的一种规范,即每个线程都有自己的工作空间,线程对变量的操作都在线程的工作内存中完成,再同步到主存中,这样可能会导致不同的线程对共享变量的操作,在各自线程工作空间内不一样的问题. 而用 volatile 修饰的变量,线程对该变量的修改,会立刻刷新到主存,其它线程读取该变量时,会重新去主存读取新值.

  • Java并发编程之关键字volatile的深入解析

    目录 前言 一.可见性 二.有序性 总结 前言 volatile是研究Java并发编程绕不过去的一个关键字,先说结论: volatile的作用: 1.保证被修饰变量的可见性 2.保证程序一定程度上的有序性 3.不能保证原子性 下面,我们将从理论以及实际的案例来逐个解析上面的三个结论 一.可见性 什么是可见性? 举个例子,小明和小红去看电影,刚开始两个人都还没买电影票,小红就先去买了两张电影票,没有告诉小明.小明以为小红没买,所以也去买了两张电影票,因为他们只有两个人,所以他们只能用两张票,这就是

  • Java 并发编程:volatile的使用及其原理解析

    Java并发编程系列[未完]: •Java 并发编程:核心理论 •Java并发编程:Synchronized及其实现原理 •Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) •Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) •Java 并发编程:volatile的使用及其原理 一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchroniz

  • Java并发编程-volatile可见性详解

    前言 要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么用? volatile主要对所修饰的变量提供两个功能 可见性 防止指令重排序 <br>本篇博客主要对volatile可见性进行探讨,以后发表关于指令重排序的博文. 什么是可见性? 把JAVA内存模型(JMM)展示得很详细了,简单概括一下 1.每个Thread有一个属于自己的工作内存(可以理解为每个

  • JAVA并发中VOLATILE关键字的神奇之处详解

    并发编程中的三个概念: 1.原子性 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行. 2.可见性 对于可见性,Java提供了volatile关键字来保证可见性. 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值. 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保

  • 详解Java并发编程基础之volatile

    目录 一.volatile的定义和实现原理 1.Java并发模型采用的方式 2.volatile的定义 3.volatile的底层实现原理 二.volatile的内存语义 1.volatile的特性 2.volatile写-读建立的happens-before关系 3.volatile的写/读内存语义 三.volatile内存语义的实现 1.volatile重排序规则 2.内存屏障 3.内存屏障示例 四.volatile与死循环问题 五.volatile对于复合操作非原子性问题 一.volati

  • java并发编程关键字volatile保证可见性不保证原子性详解

    目录 关于可见性 关于指令重排 volatile关键字可以说是Java虚拟机提供的最轻量级的同步机制,但对于为什么它只能保证可见性,不保证原子性,它又是如何禁用指令重排的,还有很多同学没彻底理解 相信我,坚持看完这篇文章,你将牢牢掌握一个Java核心知识点 先说它的两个作用: 保证变量在内存中对线程的可见性禁用指令重排 每个字都认识,凑在一起就麻了 这两个作用通常很不容易被我们Java开发人员正确.完整地理解,以至于许多同学不能正确地使用volatile 关于可见性 不多bb,码来 public

随机推荐