易失性关键字有什么用?

今天上班,我在Java遇到了volatile关键字。不太熟悉,我发现了这种解释

鉴于该文章解释了相关关键字的细节,您是否曾经使用过它,或者您是否见过可以以正确方式使用此关键字的案例?

318795 次浏览

"…易失性修饰符保证任何读取字段的线程都会看到最近写入的值。"乔什·布洛赫

如果您正在考虑使用volatile,请阅读处理原子行为的包java.util.concurrent

维基百科关于单例模式的帖子显示使用中的挥发性。

当然可以。(不仅在Java中,在C#中也是如此。)有时你需要获取或设置一个保证在给定平台上是原子操作的值,例如int或boolean,但不需要线程锁定的开销。易失性关键字允许您确保在读取值时获得当前值,而不是刚刚被另一个线程写入过时的缓存值。

volatile有内存可见性的语义学。基本上,volatile字段的值在写入操作完成后对所有读取器(特别是其他线程)可见。没有volatile,读者可以看到一些未更新的值。

回答你的问题:是的,我使用volatile变量来控制某些代码是否继续循环。循环测试volatile值,如果它是true则继续。可以通过调用“停止”方法将条件设置为false。循环看到false,并在停止方法完成执行后测试值时终止。

我强烈推荐的书《Java实践中的并发》很好地解释了volatile。这本书是由写了问题中引用的IBM篇文章的同一个人写的(事实上,他在那篇文章的底部引用了他的书)。我对volatile的使用是他的文章所说的“模式1状态标志”。

如果你想了解更多关于volatile在幕后是如何工作的,请阅读Java记忆模型。如果你想超越这个水平,请查看一本好的计算机架构书籍,如Hennessy&Patterson,并阅读有关高速缓存一致性和高速缓存一致性的内容。

使用volatile的一个常见示例是使用volatile boolean变量作为标志来终止线程。如果您已经启动了一个线程,并且您希望能够安全地从不同的线程中断它,您可以让线程定期检查标志。要阻止它,请将标志设置为true。通过使标志volatile,您可以确保正在检查它的线程在下次检查它时会看到它已设置,甚至不必使用synchronized块。

是的,每当您希望多个线程访问可变变量时,都必须使用易失性。它不是很常见,因为通常您需要执行多个原子操作(例如,在修改变量状态之前检查它),在这种情况下,您将使用同步块。

如果您正在开发多线程应用程序,您将需要使用“易失性”关键字或“同步”以及您可能拥有的任何其他并发控制工具和技术。此类应用程序的示例是桌面应用程序。

如果你正在开发一个将部署到应用服务器(Tomcat、JBoss AS、Glassfish等)的应用程序,你不必自己处理并发控制,因为它已经由应用服务器解决了。事实上,如果我没记错的话,JavaEE标准禁止servlet和EJB中的任何并发控制,因为它是“基础设施”层的一部分,你应该免于处理它。如果你正在实现单例对象,你只能在这样的应用程序中进行并发控制。如果你使用像Spring这样的框架编织你的组件,这甚至已经解决了。

因此,在Java开发的大多数情况下,应用程序是Web应用程序并使用Spring或EJB等IoC框架,您不需要使用“易失性”。

是的,我经常使用它——它对多线程代码非常有用。你指出的文章是一篇很好的文章。尽管有两件重要的事情要记住:

  1. 只有在以下情况下才应该使用易失性完全理解它的作用以及它与同步的不同之处。在许多情况下易变的出现,从表面上看,更简单高性能替代同步,通常是更好的了解挥发性会使明确同步是唯一的#36825;行的选择。
  2. 易失性实际上不工作在一个很多旧的JVM,虽然同步的。我记得看到一个文档,它引用了不同JVM中的不同支持级别,但不幸的是我现在找不到它。如果您使用的是1.5之前Java版本,或者您无法控制程序将在其上运行的JVM,请务必查看它。

volatile对于停止线程非常有用。

并不是说你应该编写自己的线程,Java1.6有很多不错的线程池。但是如果你确定你需要一个线程,你需要知道如何停止它。

我用于线程的模式是:

public class Foo extends Thread {
private volatile boolean close = false;
public void run() {while(!close) {// do work}}public void close() {close = true;// interrupt here if needed}}

在上面的代码段中,在这时候循环中读取close的线程与调用close()的线程不同。如果没有易失性,运行循环的线程可能永远不会看到关闭的更改。

注意不需要同步

volatile只保证所有线程,甚至它们自己,都是递增的。例如:计数器同时看到变量的同一面。它不被使用而不是同步或原子或其他东西,它完全使读取同步。请不要将其与其他java关键字进行比较。正如下面的示例所示,易失性变量操作也是原子的,它们一次失败或成功。

package io.netty.example.telnet;
import java.util.ArrayList;import java.util.List;
public class Main {
public static volatile  int a = 0;public static void main(String args[]) throws InterruptedException{
List<Thread> list = new  ArrayList<Thread>();for(int i = 0 ; i<11 ;i++){list.add(new Pojo());}
for (Thread thread : list) {thread.start();}
Thread.sleep(20000);System.out.println(a);}}class Pojo extends Thread{int a = 10001;public void run() {while(a-->0){try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}Main.a++;System.out.println("a = "+Main.a);}}}

即使你放易失性或不结果总是会有所不同。但是如果你像下面一样使用原子整数,结果将总是相同的。这与同步也是一样的。

    package io.netty.example.telnet;
import java.util.ArrayList;import java.util.List;import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static volatile  AtomicInteger a = new AtomicInteger(0);public static void main(String args[]) throws InterruptedException{
List<Thread> list = new  ArrayList<Thread>();for(int i = 0 ; i<11 ;i++){list.add(new Pojo());}
for (Thread thread : list) {thread.start();}
Thread.sleep(20000);System.out.println(a.get());
}}class Pojo extends Thread{int a = 10001;public void run() {while(a-->0){try {Thread.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}Main.a.incrementAndGet();System.out.println("a = "+Main.a);}}}

在我看来,除了停止线程之外,还有两个重要的场景,其中使用了易失性关键字:

  1. 双重检查锁定机构。经常用于单例设计模式。在这个单例对象需要声明为易失性
  2. 虚假唤醒。即使没有发出通知调用,线程有时也会从等待调用中唤醒。这种行为称为虚假唤醒。这可以通过使用条件变量(布尔标志)来对抗。只要该标志为真,就将等待()调用放入这时候循环中。因此,如果线程由于通知/通知所有以外的任何原因从等待调用中唤醒,那么它遇到的标志仍然为真,因此再次调用等待。在调用通知之前,将此标志设置为真。在这种情况下,boolean标志被声明为易失性

每个访问易失性字段的线程在继续之前都会读取其当前值,而不是(可能)使用缓存值。

只有成员变量可以是易失性或瞬态的。

没有人提到长变量和双变量类型的读写操作的处理。对于引用变量和大多数原始变量,读写都是原子操作,但long和双变量类型除外,它们必须使用易失性关键字才能是原子操作。@陈志立

在正常温度下容易蒸发

关于volatile的要点:

  1. Java中的同步可以通过使用Java关键字synchronizedvolatile以及锁来实现。
  2. 在Java,我们不能有synchronized变量。使用synchronized关键字与变量是非法的,将导致编译错误。您可以使用javavolatile变量,而不是在Java中使用synchronized变量,这将指示JVM线程从主存储器读取volatile变量的值,而不是将其缓存在本地。
  3. 如果一个变量没有在多个线程之间共享,那么就没有必要使用volatile关键字。

volatile的用法示例:

public class Singleton {private static volatile Singleton _instance; // volatile variablepublic static Singleton getInstance() {if (_instance == null) {synchronized (Singleton.class) {if (_instance == null)_instance = new Singleton();}}return _instance;}}

我们在第一个请求到来时懒惰地创建实例。

如果我们不将_instance变量设为volatile,那么创建Singleton实例的线程就无法与其他线程通信。因此,如果线程A正在创建Singleton实例,并且刚刚创建之后,CPU损坏等,所有其他线程将无法看到_instance的值不是null,他们会相信它仍然被分配为null。

为什么会发生这种情况?因为读取器线程没有做任何锁定,并且在写入器线程从同步块中出来之前,内存不会被同步,主存储器中的_instance值也不会更新。在Java中使用Volatile关键字,这是由Java自己处理的,这样的更新将对所有读取器线程可见。

结论volatile关键字也用于在线程之间传递内存内容。

无挥发性的用法示例:

public class Singleton {private static Singleton _instance;   //without volatile variablepublic static Singleton getInstance() {if (_instance == null) {synchronized(Singleton.class) {if (_instance == null)_instance = new Singleton();}}return _instance;}}

上面的代码不是线程安全的。尽管它在同步块中再次检查实例的值(出于性能原因),但即时编译器可以重新排列字节码,使对实例的引用在构造函数完成执行之前设置。这意味着方法getInstance()返回一个可能尚未完全初始化的对象。为了使代码线程安全,从Java5开始就可以使用关键字易失性来表示实例变量。标记为易失性的变量只有在对象的构造函数完全完成执行后才对其他线程可见。
来源

在此处输入图片描述

#0Java

快速失败迭代器是使用列表对象上的volatile计数器通常实现的。

  • 当列表更新时,计数器递增。
  • 创建Iterator时,计数器的当前值嵌入到Iterator对象中。
  • 当执行Iterator操作时,该方法比较两个计数器值,如果它们不同,则抛出ConcurrentModificationException

故障安全迭代器的实现通常是轻量级的。它们通常依赖于特定列表实现的数据结构的属性。没有通用模式。

当与变量一起使用时,易失性键将确保读取此变量的线程将看到相同的值。现在,如果有多个线程读取和写入一个变量,使变量易失性是不够的,数据将被损坏。映像线程读取了相同的值,但每个线程都做了一些变化(例如增加计数器),当写回内存时,会侵犯数据完整性。这就是为什么有必要使变量同步(可能有不同的方法)

如果更改是由1个线程完成的,而其他线程只需要读取此值,则易失性将是合适的。

从oracle留档页面开始,出现了对易失性变量的需求来修复内存一致性问题:

使用易失性变量可以降低内存一致性错误的风险,因为对易失性变量的任何写入都会与该变量的后续读取建立发生之前的关系。

这意味着对volatile变量的更改对其他线程总是可见的。这也意味着当线程读取易失性变量时,它不仅会看到volatile的最新更改,还会看到导致更改的代码的副作用。

正如Peter Parker答案中解释的那样,在没有volatile修饰符的情况下,每个线程的堆栈可能有自己的变量副本。通过将变量设为volatile,内存一致性问题得到了解决。

查看jenkov教程页面以更好地理解。

查看相关的SE问题,了解有关易失性和使用易失性的用例的更多详细信息:

Java

一个实际用例:

你有许多线程,它们需要以特定的格式打印当前时间,例如:java.text.SimpleDateFormat("HH-mm-ss")。你可以有一个类,它将当前时间转换为SimpleDateFormat并每一秒钟更新一次变量。所有其他线程都可以简单地使用这个易失性变量在日志文件中打印当前时间。

Volatile Variables是轻量级同步。当需要所有线程之间最新数据的可见性并且原子性可能受到损害时,在这种情况下,Volatile Variables必须是首选。读取易失性变量总是返回任何线程最近完成的写操作,因为它们既不缓存在寄存器中,也不缓存在其他处理器看不到的缓存中。Volatile是无锁的。当场景符合上述条件时,我使用易失性。

有两种不同的使用挥发性关键字。

  1. 防止JVM从寄存器中读取值(假设为缓存),并强制从内存中读取其值。
  2. 降低内存不一致错误的风险。

阻止JVM读取寄存器中的值,并强制其要从内存中读取的值。

繁忙的旗帜用于防止线程在设备繁忙且标志不受锁保护时继续:

while (busy) {/* do something else */}

当另一个线程关闭繁忙的旗帜时,测试线程将继续:

busy = 0;

然而,由于在测试线程中频繁访问忙,JVM可以通过将忙的值放在寄存器中来优化测试,然后在每次测试前测试寄存器的内容而不读取内存中忙的值。测试线程永远不会看到忙的变化,另一个线程只会更改内存中忙的值,从而导致死锁。声明繁忙的旗帜为易失性强制在每次测试前读取其值。

降低内存一致性错误的风险。

使用易失性变量可以降低内存一致性错误的风险,因为对易失性变量的任何写入都会建立一个与同一变量的后续读取之间的关系。这意味着对易失性变量的更改始终对其他线程可见。

读,写没有内存一致性错误的技术被称为原子作用

原子作用是一种有效地同时发生的作用。原子作用不能在中间停止:它要么完全发生,要么根本不发生。在动作完成之前,原子作用的副作用是不可见的。

以下是您可以指定的原子操作:

  • 对于引用变量和大多数引用变量,读和写是原子的原始变量(除了long和double之外的所有类型)。
  • 对于声明为的所有变量,读取和写入都是原子的(包括long和double变量)。

干杯!

使用 volatile关键字声明的变量有两个特殊的主要特性。

  1. 如果我们有一个 Volatile变量,它不能被任何线程缓存到计算机(微处理器)的缓存内存中。总是从主存进行访问。

  2. 如果有一个 写操作 Volatile变量,突然一个 读取操作被请求,它保证 写操作将在读操作之前完成

以上两个特性推断出

  • 所有读取 Volatile变量的线程肯定会读取最新值。因为任何缓存的值都不会污染它。而且只有在当前写操作完成之后才会授予读请求。

另一方面,

  • 如果我们进一步研究我提到的 # 2,我们可以看到,volatile关键字是维护共享变量的理想方法,它有 N 个读线程和只有一个写线程来访问它。一旦我们添加了 volatile关键字,就完成了。没有关于线程安全的其他开销。

相反,

我们 不能单独使用 volatile关键字,以满足一个共享变量的 不止一个写线程访问它

易波动性会随之而来。

不同线程对易失性变量的读写总是来自内存,而不是来自线程自己的缓存或 CPU 寄存器。所以每个线程总是处理最新的值。 2 > 当两个不同的线程在堆中使用同一个实例或静态变量时,可能会看到其他线程的操作无序。看看 Jeremy Manson 的博客吧。但是反复无常有帮助。

以下完全运行的代码显示了一些线程如何在不使用 synized 关键字的情况下按预定义的顺序执行和打印输出。

thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3

为了实现这一点,我们可以使用以下完整的运行代码。

public class Solution {
static volatile int counter = 0;
static int print = 0;
public static void main(String[] args) {
// TODO Auto-generated method stub
Thread[] ths = new Thread[4];
for (int i = 0; i < ths.length; i++) {
ths[i] = new Thread(new MyRunnable(i, ths.length));
ths[i].start();
}
}
static class MyRunnable implements Runnable {
final int thID;
final int total;
public MyRunnable(int id, int total) {
thID = id;
this.total = total;
}
@Override
public void run() {
// TODO Auto-generated method stub
while (true) {
if (thID == counter) {
System.out.println("thread " + thID + " prints " + print);
print++;
if (print == total)
print = 0;
counter++;
if (counter == total)
counter = 0;
} else {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
// log it
}
}
}
}
}
}

下面的 github 链接有一个自述文件,它给出了正确的解释。 Https://github.com/sankar4git/volatile_thread_ordering

下面是一个非常简单的代码,它演示了 volatile对变量的要求,这个变量用于控制来自其他线程的 Thread 执行(这是一个需要 volatile的场景)。

// Code to prove importance of 'volatile' when state of one thread is being mutated from another thread.
// Try running this class with and without 'volatile' for 'state' property of Task class.
public class VolatileTest {
public static void main(String[] a) throws Exception {
Task task = new Task();
new Thread(task).start();


Thread.sleep(500);
long stoppedOn = System.nanoTime();


task.stop(); // -----> do this to stop the thread


System.out.println("Stopping on: " + stoppedOn);
}
}


class Task implements Runnable {
// Try running with and without 'volatile' here
private volatile boolean state = true;
private int i = 0;


public void stop() {
state = false;
}


@Override
public void run() {
while(state) {
i++;
}
System.out.println(i + "> Stopped on: " + System.nanoTime());
}
}


当不使用 volatile时: 即使在“ 停在: xxx”之后你也不会看到“ 停在: xxx”消息,而且程序会继续运行。

Stopping on: 1895303906650500

volatile使用: 时,您将立即看到‘ 停在: xxx’。

Stopping on: 1895285647980000
324565439> Stopped on: 1895285648087300

演示: https://repl.it/repls/SilverAgonizingObjectcode

Volatile变量在主共享缓存线路更新后基本上用于即时更新(刷新) ,所以更改会立即反映到所有工作线程。

反复无常

- > synchronized< sup > [ About ]

volatile对程序员来说,值总是最新的。问题是这个值可以保存在不同类型的硬件内存中。例如,它可以是 CPU 寄存器,CPU 缓存,RAM... PU 寄存器和 CPU 缓存属于 CPU,不能共享一个数据不同的 RAM 是在多线程环境中抢救

enter image description here

关键字 volatile表示一个变量将是从/到 RAM 内存 直接读和写

Java 5通过支持 happens-before< sup > [ About ] 扩展了 volatile

对易失性字段的写操作发生在该字段的每次后续读操作之前。

Read is after write

volatile关键字 无法治愈一个 race condition的情况下,几个线程可以同时 写作一些值。答案是 synchronized关键字 < sup > [ About ]

因此,只有当 线程 写作和其他线程只读取 volatile值时,它才是安全的

虽然我在这里提到的答案中看到了许多很好的理论解释,但我在这里增加了一个实际的例子来解释:

1.

运行代码而不使用易失性

public class VisibilityDemonstration {


private static int sCount = 0;


public static void main(String[] args) {
new Consumer().start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
return;
}
new Producer().start();
}


static class Consumer extends Thread {
@Override
public void run() {
int localValue = -1;
while (true) {
if (localValue != sCount) {
System.out.println("Consumer: detected count change " + sCount);
localValue = sCount;
}
if (sCount >= 5) {
break;
}
}
System.out.println("Consumer: terminating");
}
}


static class Producer extends Thread {
@Override
public void run() {
while (sCount < 5) {
int localValue = sCount;
localValue++;
System.out.println("Producer: incrementing count to " + localValue);
sCount = localValue;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
return;
}
}
System.out.println("Producer: terminating");
}
}
}

在上面的代码中,有两个线程-Producer 和 Consumer。

生成器线程在循环中迭代5次(休眠时间为1000毫秒或1秒)。在每次迭代中,生成器线程将 sCount 变量的值增加1。因此,生成器在所有迭代中将 sCount 的值从0更改为5

使用者线程处于一个常量循环中,每当 sCount 的值发生更改时就打印该值,直到该值到达其结束的位置5为止。

两个循环同时启动。因此,生产者和消费者都应该将 sCount 的值打印5次。

输出

Consumer: detected count change 0
Producer: incrementing count to 1
Producer: incrementing count to 2
Producer: incrementing count to 3
Producer: incrementing count to 4
Producer: incrementing count to 5
Producer: terminating

分析

在上面的程序中,当生产者线程更新 sCount 的值时,它会更新主内存(每个线程最初读取变量值的地方的内存)中变量的值。但是使用者线程只是第一次从这个主内存读取 sCount 的值,然后将该变量的值缓存到它自己的内存中。因此,即使生产者线程已经更新了主存中原始 sCount 的值,使用者线程仍然在读取其未更新的缓存值。这就是 能见度问题

2.

使用易失性运行代码

在上面的代码中,用以下代码替换声明 sCount 的代码行:

private volatile  static int sCount = 0;

输出

Consumer: detected count change 0
Producer: incrementing count to 1
Consumer: detected count change 1
Producer: incrementing count to 2
Consumer: detected count change 2
Producer: incrementing count to 3
Consumer: detected count change 3
Producer: incrementing count to 4
Consumer: detected count change 4
Producer: incrementing count to 5
Consumer: detected count change 5
Consumer: terminating
Producer: terminating

分析

当我们声明一个变量时,这意味着所有对这个变量或从这个变量读取和写入的操作都将直接进入主存。这些变量的值永远不会被缓存。

由于 sCount 变量的值永远不会被任何线程缓存,因此使用者总是从主内存中读取 sCount 的原始值(在主内存中,生产者线程正在对其进行更新)。因此,在这种情况下,输出是正确的,其中两个线程打印 sCount 的不同值5次。

通过这种方式,volatile 关键字解决了 能见度问题问题。

假设一个线程修改了一个共享变量的值,如果您没有为该变量使用 volatile修饰符的话。当其他线程想要读取这个变量的值时,它们看不到更新的值,因为它们从 CPU 的缓存而不是 RAM 内存中读取变量的值。这个问题也称为 Visibility Problem

通过声明共享变量 volatile,对计数器变量的所有写操作将立即写回主内存。此外,计数器变量的所有读取将直接从主存读取。

public class SharedObject {
public volatile int sharedVariable = 0;
}

对于非易失性变量,Java 虚拟机(JVM)无法保证何时将数据从主内存读取到 CPU 缓存,或将数据从 CPU 缓存写入到主内存。这可能会导致几个问题,我将在下面几节中解释。


例如:

假设有这样一种情况,两个或多个线程可以访问一个共享对象,该对象包含一个计数器变量,声明如下:

public class SharedObject {
public int counter = 0;
}

再想象一下,只有线程1递增计数器变量,但是线程1和线程2都可以不时地读取计数器变量。

如果计数器变量未声明为易失性,则不能保证何时将计数器变量的值从 CPU 缓存写回主内存。这意味着 CPU 缓存中的计数器变量值可能不同于主存中的计数器变量值。这种情况在这里举例说明:

volatile

线程没有看到变量的最新值,因为它还没有被另一个线程写回主内存,这个问题称为“可见性”问题。一个线程的更新对其他线程不可见。

如果您有一个多线程系统,并且这些多线程处理某些共享数据,那么这些线程将在它们自己的缓存中加载数据。如果我们不锁定资源,那么在一个线程中所做的任何更改都不会在另一个线程中可用。

enter image description here

使用锁定机制,我们向数据源添加读/写访问。如果一个线程修改了数据源,那么该数据将存储在主内存中,而不是其缓存中。当其他线程需要这些数据时,它们将从主内存中读取这些数据。这将极大地增加延迟。

为了减少延迟,我们将变量声明为 volatile。这意味着每当在任何一个处理器中修改变量的值时,其他线程将被迫读取它。它仍然有一些延迟,但比从主存读取要好。