volatile
关键字。由于不是很熟悉它,我找到了这篇说明文章。鉴于该文章详细解释了关键字的相关内容,您是否曾经使用过它,或者是否可以想象出可以正确使用该关键字的情况?
volatile
关键字。由于不是很熟悉它,我找到了这篇说明文章。volatile
修饰符,则当其他线程想要读取此变量的值时,它们不会看到更新后的值,因为它们从CPU的缓存而不是RAM内存中读取变量的值。这个问题也被称为可见性问题
。volatile
,所有对计数器变量的写入将立即写回主内存。此外,所有对计数器变量的读取将直接从主内存中读取。public class SharedObject {
public volatile int sharedVariable = 0;
}
例子:
想象一种情况,其中两个或更多线程可以访问一个包含计数器变量的共享对象,该变量声明如下:
public class SharedObject {
public int counter = 0;
}
volatile
只能保证所有线程(包括自己)都在增加。例如:计数器同时看到变量的同一面。它不是用来代替synchronized、atomic或其他东西的,它完全使读取同步。请不要将其与其他Java关键字进行比较。正如下面的示例所示,volatile变量操作也是原子的,它们会一次性失败或成功。
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关键字,结果都会不同。但是如果您像下面这样使用AtomicInteger,则结果将始终相同。这与synchronized关键字的作用相同。
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.
不使用VOLATILE运行代码
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");
}
}
}
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.
使用volatile关键字运行的代码
在上面的代码中,将声明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的原始值(在那里它被生产者线程更新)。 因此,在这种情况下,输出是正确的,两个线程各自打印了5次sCount的不同值。
通过这种方式,volatile关键字解决了可见性问题。
访问 volatile 字段的每个线程都会在继续之前读取其当前值,而不是(可能)使用缓存值。
只有成员变量可以是 volatile 或 transient。
是的,我经常使用它 - 它对于多线程代码非常有用。你指出的那篇文章很不错。但要注意两件重要的事情:
volatile关键字有两种不同的用法。
防止JVM从寄存器中读取值,并强制其值从内存中读取。
一个繁忙标志用于在设备忙碌时阻止线程继续执行,而该标志未受锁保护:
while (busy) {
/* do something else */
}
当另一个线程关闭“忙碌标志”时,测试线程将继续执行:
busy = 0;
volatile关键字的作用如下:
1> 不同线程对volatile变量的读写总是来自内存,而不是线程自己的缓存或CPU寄存器。因此,每个线程总是处理最新的值。 2> 当两个不同的线程在堆中使用相同的实例或静态变量时,一个线程可能会看到另一个线程的操作顺序混乱。参见Jeremy Manson的博客。但是,使用volatile可以解决这个问题。
以下完整运行的代码展示了多个线程如何按照预定义的顺序执行并输出结果,而不使用synchronized关键字。
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
}
}
}
}
}
}