4

我一直在阅读有关volatile在 Java 中使用变量的信息。我了解他们确保即时查看系统中运行在不同内核/处理器上的所有线程的最新更新。但是,不能确保导致这些更新的操作的原子性。我看到以下文献经常被使用

对 volatile 字段的写入发生在每次读取同一字段之前。

这是我有点困惑的地方。这是一段代码,可以帮助我更好地解释我的查询。

volatile int x = 0;
volatile int y = 0; 

Thread-0:                       |               Thread-1:
                                |
if (x==1) {                     |               if (y==1) {
     return false;              |                    return false; 
} else {                        |               } else {
     y=1;                       |                   x=1;
     return true;               |                   return true;
}                               |               }

由于 x & y are both volatile,我们有以下发生前的边缘

  1. 在 Thread-0 中写入 y 和在 Thread-1 中读取 y 之间
  2. 在 Thread-1 中写入 x 和在 Thread-0 中读取 x 之间

这是否意味着,在任何时候,只有一个线程可以在其“else”块中(因为写入会在读取之前发生)?

很有可能 Thread-0 启动,加载 x,发现它的值为 0,就在它即将在 else 块中写入 y 之前,有一个上下文切换到 Thread-1,它加载 y 发现它的值为 0因此也进入了 else 块。是否volatile防止这种上下文切换(似乎不太可能)?

4

7 回答 7

5

所以我认为这个问题有点杂草,要点是volatile表明变量的值可能会在当前线程的范围之外发生变化,并且在使用之前必须始终读取它的值。

原则上,您引用的语句实际上是在用当前线程替换值之前,将读取该值。

您的示例是一个竞争条件,两个线程都可能返回 true,都可能返回 true,或者它们可能各自返回不同的值 - 的语义volatile不会为您的示例定义执行(我鼓励您编译并运行它并看到输出变化)。

说明其行为的一种常见方法volatile是运行两个线程,其中一个线程更新共享状态,并查看标记字段时会发生什么情况,何时未标记:

class VolatileTest implements Runnable
{
        // try with and without volatile
        private volatile boolean stopRunning = false;

        public void triggerStop(){
             stopRunning = true;
        }

        @Override
        public void run(){
             while(!stopRunning);
             System.out.println("Finished.");
        }

        public static void main (String[] args) throws java.lang.Exception
        {
            final VolatileTest test = new VolatileTest();
            new Thread(test).start();
            Thread.sleep(1000);
            test.triggerStop() = false;
        }
}

stopRunning在此示例中,标记为失败volatile可能导致while循环永远继续,因为除非stopRunning标记为volatile不需要在每次迭代中读取值。

于 2012-09-22T03:04:13.803 回答
2

易失的语义

您所指的问题是Dekker's Algorithm 的一种变体。谷歌上有很多关于不同实现的细节和关于它的细节。

如果两个进程同时尝试进入临界区,算法将只允许一个进程进入,这取决于轮到谁。如果一个进程已经在临界区,另一个进程将忙于等待第一个进程退出。这是通过使用两个标志来完成的,标志 [0] 和标志 [1],指示进入临界区的意图,以及指示谁在两个进程之间具有优先级的轮流变量。

维基百科涵盖了volatile与 Dekker 算法的相关性

易失性信息

但我发现这篇文章volatile用一句话就能完美解释。

如果一个变量被声明为 volatile,那么可以保证任何读取该字段的线程都会看到最近写入的值。(拉斯沃格尔,2008 年)

本质上, volatile 用于指示变量的值将被不同的线程修改。(javamex,2012)

梅西大学:并发易失性讲座幻灯片

梅西讲座幻灯片
(来源:iforce.co.nz

资料来源:Hans W. Guesgen 教授

如果你还不明白volatile看看是如何atomicity工作的。

于 2012-09-22T03:22:30.320 回答
1

这是否意味着,在任何时候,只有一个线程可以在其“else”块中(因为写入会在读取之前发生)?

不,不是的。

volatile 是否可以防止这种上下文切换(似乎不太可能)?

不,它不会对此进行防范。您确定的竞争条件确实存在。

这表明 volatile 不是通用同步机制。相反,它们允许您避免在某些情况下进行同步,这些情况围绕可以在单个内存操作中读取或写入的单个变量进行。

在您的示例中,有两个变量...

于 2012-09-22T03:35:59.253 回答
0

我认为你在这里有一个不好的例子。两个线程都可以很容易地进入 else 块(线程 0 读取 x 并检查 1,然后线程 1 读取 y 并检查 1)。

感兴趣的例子是这样的:

thread 0          thread 1

x = 1             y = 1
if (y == 0) {     if (x == 0) {
}                 }

如果并且是易失性的,则两个线程都不能进入if块。xy

于 2012-09-22T03:04:21.167 回答
0

在 Java 语言规范中,volatile 字段具有以下属性:

一个字段可能被声明为 volatile,在这种情况下,线程必须在每次访问该变量时将其字段的工作副本与主副本进行协调。此外,代表线程对一个或多个易失性变量的主副本的操作由主存储器完全按照线程请求的顺序执行。

综上所述,

这是否意味着,在任何时候,只有一个线程可以在其“else”块中(因为写入会在读取之前发生)?

这不会发生,因为 volatile 确保线程正在读取内存中变量的最新值,而不是从它拥有的本地副本中读取。通过将变量声明为 volatile 是不可能阻塞的。您应该使用该synchronized概念来序列化对变量的写入。

于 2012-09-22T03:05:03.880 回答
0

所有变量上的所有 volatile 读取和写入都按总顺序(同步顺序)。

对 volatile 变量 v 的写入发生在任何线程对 v 的所有后续读取之前(其中“后续”根据同步顺序定义)

http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.4

于 2012-09-22T03:11:24.163 回答
0

易失性保证可见性,同步性保证可见性和原子性

它提供的唯一保证是如果xy由一个线程更新,则更新将对所有线程可见。执行顺序完全取决于线程的运行方式。上下文切换问题的答案是否定的,因为比较是在进入之前发生的,所以它只会是一次,它将基于当时变量的值。如果您对同一变量进行多次比较,这可能是可能的。

使用 volatile 的最佳方式是维护类的状态变量。任何新分配的位置将反映在访问该类的所有线程中

如果你看 ThreadPoolExecutor几个状态变量的源代码都是易失的。

其中之一恰好是runState决定ThreadPoolExecutor. 现在何时shutdown()调用runState被更新为,SHUTDOWN 以便接受任务的方法execute submit停止接受任务,这恰好是一个简单的任务if(runState==RUNNING)

下面的代码是ThreadPoolExecutorexecute的方法

poolSize在下面的代码中,如果您看到,变量不需要额外的同步,runState因为它们是易失的状态变量。

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    if (**poolSize** >= corePoolSize || !addIfUnderCorePoolSize(command)) {//poolSize  volatile
        if (**runState** == RUNNING && workQueue.offer(command)) {//runstate volatile
            if (**runState** != RUNNING || **poolSize** == 0)//runstate volatile
                ensureQueuedTaskHandled(command);
        }
        else if (!addIfUnderMaximumPoolSize(command))
            reject(command); // is shutdown or saturated
    }
  }

什么时候不使用?

当需要在原子操作中使用变量或当一个变量值依赖于另一个变量值时,例如 Increment i++。

选择

原子变量被称为更好的易失性,它的内存语义为易失性,但为您提供额外的操作。例如AtomicInteger 提供的incrementAndGet() 操作。

于 2012-09-22T03:37:17.937 回答