6

如果我阅读有关多线程的完整章节/书籍,我可以找到答案,但我想要一个更快的答案。(我知道这个stackoverflow 问题很相似,但还不够。)

假设有这个类:

public class TestClass {
   private int someValue;

   public int getSomeValue() { return someValue; }
   public void setSomeValue(int value) {  someValue = value; }
}

有两个线程(A 和 B)访问这个类的实例。考虑以下顺序:

  1. 答:getSomeValue()
  2. B:setSomeValue()
  3. 答:getSomeValue()

如果我是对的, someValue 必须是易失的,否则第三步可能不会返回最新值(因为 A 可能有缓存值)。它是否正确?

第二种情况:

  1. B:setSomeValue()
  2. 答:getSomeValue()

在这种情况下,A 总是会得到正确的值,因为这是它的第一次访问,所以他还没有缓存值。这是正确的吗?

如果仅以第二种方式访问​​一个类,则不需要 volatile/synchronization,或者是吗?

请注意,此示例已简化,实际上我想知道复杂类中的特定成员变量和方法,而不是整个类(即哪些变量应该是可变的或具有同步访问权限)。要点是:如果更多线程访问某些数据,是否需要同步访问,还是取决于它们访问它的方式(例如顺序)?


阅读评论后,我尝试用另一个例子来说明我的困惑的根源:

  1. 从 UI 线程:threadA.start()
  2. threadA 调用getSomeValue(),并通知 UI 线程
  3. UI 线程获取消息(在其消息队列中),因此它调用:threadB.start()
  4. threadB 调用setSomeValue(),并通知 UI 线程
  5. UI 线程获取消息,并通知线程A(以某种方式,例如消息队列)
  6. 线程A调用getSomeValue()

这是一个完全同步的结构,但为什么这意味着 threadA 将在第 6 步中获得最新的值?(如果someValue不是易失性的,或者从任何地方访问时都没有放入监视器)

4

5 回答 5

3

如果两个线程调用相同的方法,则无法保证调用所述方法的顺序。因此,取决于调用顺序的原始前提是无效的。

这与调用方法的顺序无关;这是关于同步的。这是关于使用某种机制使一个线程等待而另一个完全完成其写操作。 一旦决定拥有多个线程,就必须 提供同步机制以避免数据损坏。

于 2012-06-29T22:35:48.290 回答
2

众所周知,我们需要保护的是数据的关键状态,而支配数据关键状态的原子语句必须是同步的。

我有这个例子,其中使用了 volatile,然后我使用了 2 个线程,这些线程用于每次将计数器的值增加 1 直到 10000。所以它必须总共是 20000。但令我惊讶的是它并没有总是发生。

然后我使用同步关键字使其工作。

同步确保当线程访问同步方法时,不允许其他线程访问该对象或该对象的任何其他同步方法,确保不会发生数据损坏。

线程安全类意味着它将在存在下划线运行时环境的调度和交错的情况下保持其正确性,而无需访问该类的客户端的任何线程安全机制。

于 2012-06-30T03:06:22.847 回答
2

让我们看看这本书。

一个字段可能被声明为 volatile,在这种情况下,Java 内存模型(第 17 节)确保所有线程都能看到变量的一致值。

所以volatile保证声明的变量不会被复制到线程本地存储中,否则这是允许的。进一步解释说,这是对非常简单的共享存储同步访问的锁定替代方案。

另请参阅这篇较早的文章,该文章解释了int访问必须是原子的(但不是doubleor long)。

这些一起意味着如果您的int字段被声明volatile,则不需要锁来保证原子性:您将始终看到最后写入内存位置的值,而不是由于半完成写入而导致的混淆值(就像使用 double或长)。

但是,您似乎暗示您的 getter 和 setter 本身是原子的。这不能保证。JVM 可以在调用或返回序列的中间点中断执行。在此示例中,这没有任何后果。但是如果电话有副作用,例如setSomeValue(++val),那么你会有不同的故事。

于 2012-06-30T03:41:51.163 回答
1

问题是java只是一个规范。有很多 JVM 实现和物理操作环境的例子。在任何给定的组合上,一个动作可能是安全的或不安全的。例如,在单处理器系统上,您示例中的 volatile 关键字可能完全没有必要。由于内存和语言规范的编写者无法合理地考虑可能的操作条件集,他们选择将某些模式列入白名单,这些模式保证适用于所有兼容的实现。遵守这些准则可确保您的代码在您的目标系统上运行,并确保它具有合理的可移植性。

在这种情况下,“缓存”通常是指在硬件级别进行的活动。在 java 中发生的某些事件会导致多处理器系统上的内核“同步”它们的缓存。访问 volatile 变量就是一个例子,同步块是另一个例子。想象一个场景,这两个线程 X 和 Y 被安排在不同的处理器上运行。

X starts and is scheduled on proc 1
y starts and is scheduled on proc 2

.. now you have two threads executing simultaneously
to speed things up the processors check local caches
before going to main memory because its expensive.

x calls setSomeValue('x-value') //assuming proc 1's cache is empty the cache is set
                                //this value is dropped on the bus to be flushed
                                //to main memory
                                //now all get's will retrieve from cache instead
                                //of engaging the memory bus to go to main memory 
y calls setSomeValue('y-value') //same thing happens for proc 2

//Now in this situation depending on to order in which things are scheduled and
//what thread you are calling from calls to getSomeValue() may return 'x-value' or
//'y-value. The results are completely unpredictable.  

关键是volatile(在兼容的实现上)确保有序写入将始终刷新到主内存,并且在下一次访问之前,其他处理器的缓存将被标记为“脏”,无论从哪个线程进行访问。

免责声明:易失性不会锁定。这一点很重要,尤其是在以下情况下:

volatile int counter;

public incrementSomeValue(){
    counter++; // Bad thread juju - this is at least three instructions 
               // read - increment - write             
               // there is no guarantee that this operation is atomic
}

setSomeValue如果您的意图是必须始终在之前调用,这可能与您的问题有关getSomeValue

如果意图是getSomeValue()必须始终反映最近的调用,setSomeValue()那么这是使用volatile关键字的好地方。请记住,如果没有它,即使首先安排了,也无法保证getSomeValue()会反映最近的调用。setSomeValue()setSomeValue()

于 2012-06-29T23:06:37.293 回答
1

如果我是对的, someValue 必须是易失的,否则第三步可能不会返回最新值(因为 A 可能有缓存值)。它是否正确?

如果线程 B 调用 setSomeValue(),您需要某种同步来确保线程 A 可以读取该值。volatile不会自行完成此操作,也不会使方法同步。执行此操作的代码最终是您添加的任何同步代码,以确保A: getSomeValue()B: setSomeValue(). 如果按照您的建议,您使用消息队列来同步线程,则会发生这种情况,因为一旦线程 B 获得消息队列上的锁,线程 A 所做的内存更改就对线程 B 可见。

如果仅以第二种方式访问​​一个类,则不需要 volatile/synchronization,或者是吗?

如果您真的在进行自己的同步,那么听起来您并不关心这些类是否是线程安全的。但请确保您不会同时从多个线程访问它们;否则,任何非原子的方法(分配 int is)都可​​能导致您处于不可预测的状态。一种常见的模式是将共享状态放入不可变对象中,这样您就可以确定接收线程没有调用任何 setter。

如果您确实有一个要更新并从多个线程读取的类,我可能会先做最简单的事情,这通常是同步所有公共方法。如果您真的认为这是一个瓶颈,您可以研究 Java 中一些更复杂的锁定机制。

那么 volatile 保证什么?

对于确切的语义,您可能必须阅读教程,但总结它的一种方法是 1)最后一个线程访问 volatile 所做的任何内存更改将对访问 volatile 的当前线程可见,以及 2)访问 volatile 是原子的(它不会是部分构造的对象,也不是部分分配的 double 或 long)。

同步块具有类似的属性:1)最后一个线程访问锁所做的任何内存更改对该线程都是可见的,并且 2)块内所做的更改相对于其他同步块以原子方式执行

(1) 表示任何内存更改,而不仅仅是对 volatile 的更改(我们谈论的是 JDK 1.5 之后)或同步块内的更改。这就是人们提到排序时的意思,这是在不同的芯片架构上以不同的方式完成的,通常是通过使用内存屏障。

此外,在同步块的情况下,(2) 仅保证如果您在同一个锁上同步的另一个块中,您不会看到不一致的值。同步对共享变量的所有访问通常是个好主意,除非您真的知道自己在做什么。

于 2012-06-30T04:17:42.833 回答