多线程访问和线程的可变缓存

时间:2012-06-29 22:27:56

标签: java android multithreading volatile

如果我读完一篇关于多线程的完整章节/书,我能找到答案,但我想要一个更快的答案。 (我知道this stackoverflow问题类似,但还不够。)

假设有这个类:

public class TestClass {
   private int someValue;

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

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

  1. A:getSomeValue()
  2. B:setSomeValue()
  3. A:getSomeValue()
  4. 如果我是对的,someValue必须是volatile,否则第3步可能不会返回最新值(因为A可能有缓存值)。这是对的吗?

    第二种情况:

    1. B:setSomeValue()
    2. A:getSomeValue()
    3. 在这种情况下,A将始终获得正确的值,因为这是它的第一次访问,因此他还没有缓存值。这是对的吗?

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

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


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

      1. 来自UI主题:threadA.start()
      2. threadA调用getSomeValue(),并通知UI线程
      3. UI线程获取消息(在其消息队列中),因此它调用:threadB.start()
      4. threadB调用setSomeValue(),并通知UI线程
      5. UI线程获取消息,并通知threadA(以某种方式,例如消息队列)
      6. threadA调用getSomeValue()
      7. 这是一个完全同步的结构,但为什么这意味着threadA将在步骤6中获得最新的值? (如果someValue不易变,或者从任何地方访问时都没有放入监视器中

5 个答案:

答案 0 :(得分:3)

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

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

答案 1 :(得分:2)

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

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

然后我使用 synchronized关键字使其正常工作。

同步确保当线程访问synchronized方法时,不允许其他线程访问该对象或该对象的任何其他同步方法,确保数据损坏没有完成。

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

答案 2 :(得分:2)

让我们看一下the book.

  

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

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

另请参阅this earlier article,,其中说明int访问必须是原子的(但不是doublelong)。

这些一起意味着如果你的int字段被声明为volatile,那么就不需要锁来保证原子性:你总会看到一个最后写入内存位置的值,而不是一些混淆的值由半完全写入产生(可能是双倍或长度)。

然而,你似乎暗示你的吸气剂和制定者本身是原子的。这不保证。 JVM可以在调用或返回序列期间的中间点中断执行。在这个例子中,这没有任何后果。但是如果这些电话有副作用,例如setSomeValue(++val),那么你会有不同的故事。

答案 3 :(得分:1)

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

在这种情况下,“缓存”通常是指在硬件级别上进行的活动。 java中存在某些事件导致多处理器系统上的核心“同步”其缓存。访问volatile变量就是一个例子,synchronized块是另一个。想象一下这两个线程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不会锁定。这在以下情况下尤为重要:

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()的最近通话。

答案 4 :(得分:1)

  

如果我是对的,someValue必须是volatile,否则第3步可能不会返回最新值(因为A可能有缓存   值)。这是对的吗?

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

  

如果仅以第二种方式访问​​类,则不需要   易失性/同步,还是它?

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

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

那么挥发性保证是什么?

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

同步块具有类似的属性:1)此线程可以看到最后一个线程访问锁所做的任何内存更改,以及2)块内的更改是相对于其他同步块原子执行的< / p>

(1)表示任何内存更改,而不仅仅是对volatile的更改(我们正在讨论JDK 1.5)或者在synchronized块内。这就是人们提到订购时的意思,这是通过不同的芯片架构以不同的方式实现的,通常是通过使用内存屏障。

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