今天工作时,我在Java中遇到了volatile关键字。不是很熟悉,我找到了这个解释。

鉴于那篇文章详细解释了所讨论的关键字,您曾经使用过它吗?或者您曾经看到过以正确方式使用该关键字的情况吗?


当前回答

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
  }
}

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

注意,这里不需要同步

其他回答

volatile关键字有两种不同的用法。

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

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

busy标志用于防止线程在设备繁忙且该标志不受锁保护的情况下继续运行:

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

当另一个线程关闭busy标志时,测试线程将继续执行:

busy = 0;

但是,由于busy在测试线程中经常被访问,JVM可以通过将busy的值放在寄存器中来优化测试,然后在每次测试之前测试寄存器的内容,而不读取内存中的busy值。测试线程永远不会看到busy的变化,而另一个线程只会改变内存中的busy值,从而导致死锁。将busy标志声明为volatile将强制在每次测试之前读取它的值。

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

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

这种读写而不产生记忆一致性错误的技术被称为原子动作。

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

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

对于引用变量和大多数变量来说,读和写都是原子的 基本变量(所有类型,除了long和double)。 对于所有声明为volatile的变量,读和写都是原子的 (包括长变量和双变量)。

干杯!

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

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");
    }
}
}

在上面的代码中,有两个线程——生产者和消费者。

生产者线程在循环中迭代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

分析

In the above program, when the producer thread updates the value of sCount, it does update the value of the variable in the main memory(memory from where every thread is going to initially read the value of variable). But the consumer thread reads the value of sCount only the first time from this main memory and then caches the value of that variable inside its own memory. So, even if the value of original sCount in main memory has been updated by the producer thread, the consumer thread is reading from its cached value which is not updated. This is called VISIBILITY PROBLEM .

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修饰符的话。当其他线程想要读取这个变量的值时,它们看不到更新后的值,因为它们是从CPU的缓存而不是RAM内存中读取变量的值。这个问题也被称为能见度问题。

通过将共享变量声明为volatile,所有对计数器变量的写入都将立即写入主存。同样,所有对counter变量的读取都将直接从主存中读取。

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

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


例子:

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

public class SharedObject {
    public int counter = 0;
}

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

如果计数器变量没有被声明为volatile,则不能保证计数器变量的值何时从CPU缓存写入主存。这意味着CPU缓存中的计数器变量值可能与主存中的不相同。这种情况如下所示:

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

Volatile执行以下操作。

不同线程对volatile变量的读写总是从内存,而不是从线程自己的缓存或cpu寄存器。所以每个线程总是处理最新的值。 2>当两个不同的线程在堆中使用相同的实例或静态变量时,其中一个线程可能会认为其他线程的操作是无序的。请看jeremy manson的博客。但不稳定在这里有所帮助。

下面完全运行的代码展示了如何在不使用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
                    }
                }
            }
        }
    }
}

下面的github链接有一个自述,它给出了适当的解释。 https://github.com/sankar4git/volatile_thread_ordering

挥发性

volatile -> synchronized[关于]

Volatile表示对于程序员来说,该值总是最新的。问题是该值可以保存在不同类型的硬件内存中。例如,它可以是CPU寄存器,CPU缓存,RAM…СPU寄存器和CPU缓存属于CPU,不能共享数据,不像RAM在多线程环境中处于抢救状态

volatile关键字表示变量将直接从/写入RAM内存。它有一些计算足迹

Java 5通过支持happens-before扩展volatile[关于]

对volatile字段的写入发生在后续每次读取该字段之前。

Read is after write

volatile关键字不能修复竞态条件[关于],使用synchronized关键字[关于]

因此,只有当一个线程写入,而其他线程只是读取volatile值时才安全