需要使用volatile关键字吗?

4
如果我有一个字节队列,期望有一个线程作为生产者,另一个作为消费者:
class ByteQueue{
    byte[] buf;
    /*volatile?*/ int readIdx;
    /*volatile?*/ int writeIdx;
    Runnable writeListener;
    Runnable readListener;
    // ...
    void write( byte[] b ){
        int wr = writeIdx;
        int rd = readIdx;
        // check consistency and free space using wr+rd
        // copy to buf, starting at wr, eventually wrap around
        // update writeIdx afterwards
        writeIdx = ( wr + b.length ) % buf.length;
        // callback to notify consumer for data available
        writeListener.run();
    }
    void read( byte[] b ){
        int wr = writeIdx;
        int rd = readIdx;
        // check consistency and available data using wr+rd
        // copy buf to b, starting at rd, eventually wrap around
        // update readIdx afterwards
        readIdx = ( rd + b.length ) % buf.length;
        // callback to notify producer for free space available
        readListener.run();
    }
    int available() { return (writeIdx - readIdx) % buf.length; }
    int free() { return buf.length - available() -1; }
    // ...
}

这种类型的队列不需要同步。
readIdx只被读线程修改,
writeIdx只被写线程修改。
readIdx == writeIdx 表示队列没有内容。
而且队列最多只能容纳 buf.length-1 字节的数据。
这些 volatile 变量是否必要,或者它们可以省略,因为只有一个线程是一个整数状态的修改者?
谢谢 Frank

2
你需要更强的同步,writeIdx 的更新必须始终在 buf 的更新之后发生。如果没有 synchronized,这将变得棘手。 - Piotr Praszmo
同意Banthar的说法。安全第一,不要走捷径。编写适当的同步控制。不要为了获得一纳秒的时间而牺牲正确性。 - sstan
你对修改的事情是正确的,但不要忘记读取操作也在进行。 - David Ehrmann
2
当其他线程正在使用read()write()方法时,是否可以调用available()?如果可以,那么它应该意味着什么?(提示:你能做的最好的事情是返回某个瞬间可用的字节数,但这不一定与调用者决定采取行动时可用的字节数相同。) - Solomon Slow
3个回答

5
如果另一个线程需要读取这个值,它就需要是volatile类型。关键字volatile告诉JVM这个值不能被缓存或重新排序,否则对它的更新可能不会对其他线程可见。
可见性问题也适用于buf数组。由于buf需要与索引一起更改,因此写入和读取方法似乎需要进行同步。同步使更改可见,并确保并发调用不会导致索引和buf内容不一致。

你可以使用 volatile 来处理缓冲区的内容。关键是要记住,读写 volatile 字段会在线程之间建立 happens-before 关系。因此,如果线程 A 在更新 volatile 字段之前在缓冲区中写入字节,然后线程 B 在检查相同的 volatile 字段之后检查缓冲区,则线程 B 应该看到线程 A 写入缓冲区的字节。但你说得对,synchronized 是更明显的方法... - Solomon Slow
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Nathan Hughes
谢谢,这很有帮助。所以我会将它们设置为volatile。不需要同步,因为writeIdx是最后更新的,读取不能在之前发生,反之亦然。 - fbenoit
@keinfarbton:是的,这就是詹姆斯在他的评论中描述的,您可以依赖于volatile变量创建的happens-before关系来确保您看到对缓冲区的更新。 - Nathan Hughes

2
您应该将它们声明为volatile。例如,让我们看一下readIdx。如果它不是volatile,则写线程优化可能会假设它从未更改,并基于该假设进行错误的优化。然而,我没有看到您在写线程中访问readIdx(或读线程中的writeIdx),除了对一些本地变量rd(或wr)的赋值。我只是假设缺少一些代码,否则您的问题并不真正有意义。

1

Nathan是正确的,不是两个线程会互相写入变量,而是这些变量本身可能永远不会对另一个线程(或CPU核心)可见

有一个有趣的队列实际上使用非易失性变量来让CPU更好地调度工作,它就是LMAX disruptor


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接