今天在工作中,我偶然发现了volatile
Java 中的关键字。不是很熟悉,我找到了这个解释。
鉴于那篇文章解释了相关关键字的详细信息,您是否曾经使用过它,或者您是否曾经看到过可以以正确方式使用该关键字的案例?
今天在工作中,我偶然发现了volatile
Java 中的关键字。不是很熟悉,我找到了这个解释。
鉴于那篇文章解释了相关关键字的详细信息,您是否曾经使用过它,或者您是否曾经看到过可以以正确方式使用该关键字的案例?
volatile
具有内存可见性的语义。基本上,一个volatile
字段的值在写入操作完成后对所有读取器(特别是其他线程)都是可见的。没有volatile
,读者可能会看到一些未更新的值。
回答您的问题:是的,我使用volatile
变量来控制某些代码是否继续循环。循环测试该volatile
值,如果是 则继续true
。可以false
通过调用“停止”方法来设置条件。循环false
在 stop 方法完成执行后测试值时看到并终止。
我强烈推荐的《Java Concurrency in Practice》一书很好地解释了volatile
. 这本书是由撰写问题中引用的 IBM 文章的同一个人撰写的(事实上,他在该文章的底部引用了他的书)。我使用的volatile
是他的文章所说的“模式 1 状态标志”。
如果您想了解更多关于幕后volatile
工作原理的信息,请阅读Java 内存模型。如果您想超越该级别,请查看像Hennessy & Patterson这样的优秀计算机体系结构书籍,并阅读有关缓存一致性和缓存一致性的内容。
关于volatile
:
synchronized
和volatile
和 锁来实现。synchronized
变量。对变量使用synchronized
关键字是非法的,会导致编译错误。除了synchronized
在 Java 中使用变量,您可以使用 javavolatile
变量,它会指示 JVM 线程volatile
从主内存中读取变量的值,而不是在本地缓存它。volatile
关键字。的示例用法volatile
:
public class Singleton {
private static volatile Singleton _instance; // volatile variable
public 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
关键字也用于在线程之间传递内存内容。
不带 volatile 的示例用法:
public class Singleton{
private static Singleton _instance; //without volatile variable
public static Singleton getInstance(){
if(_instance == null){
synchronized(Singleton.class){
if(_instance == null) _instance = new Singleton();
}
}
return _instance;
}
上面的代码不是线程安全的。尽管它在同步块中再次检查实例的值(出于性能原因),但 JIT 编译器可以重新排列字节码,以便在构造函数完成执行之前设置对实例的引用。这意味着方法 getInstance() 返回一个可能尚未完全初始化的对象。为了使代码线程安全,从 Java 5 开始可以使用关键字 volatile 作为实例变量。标记为 volatile 的变量只有在对象的构造函数完全完成执行后才对其他线程可见。
来源
volatile
Java中的用法:
快速失败迭代器通常使用volatile
列表对象上的计数器来实现。
Iterator
时,计数器的当前值嵌入到Iterator
对象中。Iterator
,该方法比较两个计数器值,ConcurrentModificationException
如果它们不同则抛出 a。故障安全迭代器的实现通常是轻量级的。它们通常依赖于特定列表实现的数据结构的属性。没有通用模式。
volatile
对于停止线程非常有用。
并不是说您应该编写自己的线程,Java 1.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
while循环中读取的线程与调用的线程不同close()
。如果没有 volatile,运行循环的线程可能永远不会看到要关闭的更改。
注意不需要同步
用关键字声明的变量volatile
有两个使其特别的主要特性。
如果我们有一个 volatile 变量,它就不能被任何线程缓存到计算机的(微处理器)缓存中。访问总是从主存储器发生。
如果对 volatile 变量进行写操作,突然请求读操作,则保证写操作将在读操作之前完成。
以上两个性质推断出
而另一方面,
volatile
,我们可以看到该关键字是维护共享变量的理想方式,该变量具有“n”个读取器线程并且只有一个写入器线程可以访问它。一旦我们添加了volatile
关键字,它就完成了。没有任何其他关于线程安全的开销。相反,
我们不能单独使用volatile
关键字来满足一个共享变量,它有多个写入线程访问它。
一个常见的使用示例volatile
是使用volatile boolean
变量作为标志来终止线程。如果您已经启动了一个线程,并且希望能够从另一个线程安全地中断它,您可以让该线程定期检查一个标志。要停止它,请将标志设置为 true。通过制作 flag volatile
,您可以确保正在检查它的线程将在下次检查它时看到它已被设置,而无需使用synchronized
块。
是的,当您希望多个线程访问可变变量时,必须使用 volatile。这不是很常见的用例,因为通常您需要执行多个原子操作(例如,在修改变量之前检查变量状态),在这种情况下,您将使用同步块。
没有人提到 long 和 double 变量类型的读写操作的处理。读取和写入对于引用变量和大多数原始变量都是原子操作,除了 long 和 double 变量类型,它们必须使用 volatile 关键字才能成为原子操作。@关联
易挥发的
volatile
-> synchronized
[关于]
volatile
对程序员说值总是最新的。问题是该值可以保存在不同类型的硬件内存中。例如,它可以是 CPU 寄存器、CPU 缓存、RAM ... СPU 寄存器和 CPU 缓存属于 CPU,不能共享数据,这与 RAM 不同,RAM 在多线程环境中处于救援状态
volatile
关键字表示将直接从 RAM 内存读取和写入变量。它有一些计算足迹
Java 5
volatile
通过支持扩展happens-before
[关于]
对 volatile 字段的写入发生在对该字段的每次后续读取之前。
Read is after write
volatile
关键字不能解决race condition
多个线程可以同时写入一些值的情况。答案是synchronized
关键词[关于]
因此,只有当一个线程写入而其他线程只读volatile
取值时它才是安全的
在我看来,除了停止使用 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”关键字或“synchronized”以及您可能拥有的任何其他并发控制工具和技术。此类应用程序的示例是桌面应用程序。
如果您正在开发将部署到应用程序服务器(Tomcat、JBoss AS、Glassfish 等)的应用程序,您不必自己处理并发控制,因为它已经由应用程序服务器处理。事实上,如果我没记错的话,Java EE 标准禁止在 servlet 和 EJB 中进行任何并发控制,因为它是“基础设施”层的一部分,您应该无需处理它。如果您正在实现单例对象,则只能在此类应用程序中进行并发控制。如果您使用像 Spring 这样的框架来编织组件,这甚至已经解决了。
因此,在应用程序是 Web 应用程序并使用 Spring 或 EJB 等 IoC 框架的大多数 Java 开发情况下,您不需要使用“volatile”。
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);
}
}
}
即使您放 volatile 或 not 结果总是会有所不同。但是,如果您使用 AtomicInteger 如下结果将始终相同。这与同步也相同。
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.
代码在没有易失性使用的情况下运行
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");
}
}
}
在上面的代码中,有两个线程——生产者和消费者。
生产者线程在循环中迭代 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
分析
当我们声明一个变量 volatile 时,这意味着对该变量或从该变量进行的所有读取和所有写入都将直接进入主存储器。这些变量的值永远不会被缓存。
由于 sCount 变量的值永远不会被任何线程缓存,因此消费者总是从主内存中读取 sCount 的原始值(它正在由生产者线程更新)。因此,在这种情况下,输出是正确的,两个线程都打印了 sCount 的不同值 5 次。
这样, volatile 关键字就解决了VISIBILITY PROBLEM 问题。
是的,我经常使用它——它对于多线程代码非常有用。你指出的文章是一篇好文章。虽然有两件重要的事情要记住:
每个访问 volatile 字段的线程都会在继续之前读取其当前值,而不是(可能)使用缓存值。
只有成员变量可以是易失的或瞬态的。
绝对没错。(不仅在 Java 中,在 C# 中也是如此。)有时您需要获取或设置一个值,该值保证是给定平台上的原子操作,例如 int 或 boolean,但不需要线程锁定的开销。volatile 关键字允许您确保在读取值时获得当前值,而不是刚刚被另一个线程上的写入过时的缓存值。
volatile 关键字有两种不同的用法。
防止 JVM 读取寄存器中的值,并强制从内存中读取其值。
繁忙标志用于防止线程在设备繁忙且该标志不受锁保护时继续:
while (busy) {
/* do something else */
}
当另一个线程关闭忙标志时,测试线程将继续:
busy = 0;
但是,由于busy在测试线程中被频繁访问,JVM可以通过将busy的值放在一个寄存器中来优化测试,然后在每次测试之前不读取内存中busy的值就测试寄存器的内容。测试线程永远不会看到busy变化,而另一个线程只会改变内存中busy的值,从而导致死锁。将繁忙标志声明为 volatile 会强制在每次测试之前读取其值。
降低内存一致性错误的风险。
使用 volatile 变量可以降低内存一致性错误的风险,因为对 volatile 变量的任何写入都会与随后读取该相同变量建立 “先发生”关系。这意味着对 volatile 变量的更改始终对其他线程可见。
没有内存一致性错误的读写技术称为原子动作。
原子动作是一次有效地发生的动作。原子动作不能在中间停止:它要么完全发生,要么根本不发生。在操作完成之前,原子操作的副作用是不可见的。
以下是您可以指定的原子操作:
干杯!
挥发物会跟随。
1> 不同线程对volatile变量的读写总是来自内存,而不是来自线程自己的缓存或者cpu寄存器。所以每个线程总是处理最新的值。2> 当 2 个不同的线程在堆中使用相同的实例或静态变量时,可能会认为其他线程的操作是无序的。请参阅 jeremy manson 的博客。但是 volatile 在这里有所帮助。
以下完全运行的代码显示了如何在不使用同步关键字的情况下按预定义的顺序执行多个线程并打印输出。
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
从 oracle 文档页面,需要 volatile 变量来修复内存一致性问题:
使用 volatile 变量可以降低内存一致性错误的风险,因为对 volatile 变量的任何写入都会与后续读取该相同变量建立起先发生关系。
这意味着对volatile
变量的更改总是对其他线程可见。这也意味着当一个线程读取一个 volatile 变量时,它不仅会看到对 的最新更改volatile
,还会看到导致更改的代码的副作用。
正如Peter Parker
答案中所解释的,在没有volatile
修饰符的情况下,每个线程的堆栈都可能有自己的变量副本。通过将变量设为volatile
,内存一致性问题已得到修复。
查看jenkov教程页面以获得更好的理解。
查看相关的 SE 问题,了解有关 volatile 和使用 volatile 的用例的更多详细信息:
Java 中 volatile 和 synchronized 的区别
一个实际用例:
您有许多线程,需要以特定格式打印当前时间,例如:java.text.SimpleDateFormat("HH-mm-ss")
. Yon 可以有一个类,它将当前时间转换SimpleDateFormat
为每秒钟更新一次的变量。所有其他线程可以简单地使用这个 volatile 变量在日志文件中打印当前时间。
易失性变量是轻量级同步。当所有线程之间的最新数据的可见性是必需的并且原子性可能会受到损害时,在这种情况下必须首选易失性变量。对 volatile 变量的读取总是返回任何线程最近完成的写入,因为它们既没有缓存在寄存器中,也没有缓存在其他处理器看不到的缓存中。易失性是无锁的。当场景符合上述标准时,我使用 volatile。
volatile 变量主要用于在主共享缓存行更新后进行即时更新(刷新),以便立即将更改反映到所有工作线程。
volatile 键与变量一起使用时,将确保读取此变量的线程将看到相同的值。现在,如果您有多个线程读取和写入一个变量,那么使变量 volatile 是不够的,并且数据将被损坏。图像线程读取了相同的值,但每个线程都进行了一些更改(比如增加了一个计数器),当写回内存时,数据完整性被破坏。这就是为什么有必要使变量同步(不同的方式是可能的)
如果更改由 1 个线程完成,而其他线程只需要读取此值,则 volatile 将是合适的。
下面是一个非常简单的代码来演示volatile
用于控制来自其他线程的线程执行的变量的要求(这是一个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
不使用时:即使在“ Stopping on: xxx ”之后,您也永远不会看到“ Stopped on: xxx ”消息,并且程序继续运行。
Stopping on: 1895303906650500
使用时volatile
:您会立即看到“ Stopped on: xxx ”。
Stopping on: 1895285647980000
324565439> Stopped on: 1895285648087300