【问题标题】:Multithreaded access and variable cache of threads线程的多线程访问和变量缓存
【发布时间】:2012-07-01 10:54:01
【问题描述】:

如果我阅读有关多线程的完整章节/书籍,我可以找到答案,但我想要一个更快的答案。 (我知道this 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 必须是 volatile,否则第三步可能不会返回最新值(因为 A 可能有缓存值)。这是正确的吗?

第二种情况:

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

在这种情况下,A 将始终获得正确的值,因为这是它的第一次访问,所以他还不能获得缓存值。是这样吗?

如果以第二种方式访问一个类,就不需要volatile/synchronization,是吗?

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


在阅读了 cmets 之后,我尝试用另一个例子来说明我的困惑的根源:

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

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

【问题讨论】:

    标签: java android multithreading volatile


    【解决方案1】:

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

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

    【讨论】:

    • "如果两个线程调用相同的方法,则无法保证调用所述方法的顺序。"--在我的用例中,有是保证。这是我软件中定义明确的过程,线程使用同步。因此,在我的具体上下文中,保证的操作顺序来自软件结构。问题是这些(正确排序的)操作是否会看到正确的值 without 使用监视器/易失性。(一个简单的例子:当线程 B 调用 getSomeValue() 时,线程 A 没有运行,例如线程 A 是开始更胖 getSomeValue() 被调用。)
    • 我听到你在说什么,但我仍然认为操作顺序无关紧要;重要的是第二个线程等待第一个线程完成其写操作。如果你的进程保证了这一点,那么我认为它可以被认为是线程安全的。
    • 换句话说,如果您已经在使用同步机制来保证线程安全(如您所说),则不必考虑保证操作顺序。
    • 我保证操作的顺序,是的,但是在第一种情况下(不使用volatile),线程A怎么知道自从他上次访问以来该值发生了变化?我的意思是,“软件”本身知道第 2 步,但 线程 A 可能不知道。
    • 我在主要问题帖子的末尾添加了另一个示例(以启用体面的格式)。
    【解决方案2】:

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

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

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

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

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

    【讨论】:

      【解决方案3】:

      我们来看看the book.

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

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

      另见this earlier article,,它解释了int 访问必须是原子的(但不是doublelong)。

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

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

      【讨论】:

        【解决方案4】:

        问题在于 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
        }
        

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

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

        【讨论】:

        • 所以如果我想在每个环境中都安全(显然,这是必须的),那么对变量的多线程访问必须同步无论操作顺序如何(即不管操作本身是否同步)? IE。即使操作顺序严格遵循我的软件结构(如我帖子末尾的示例),仍然应该有较低级别的同步(实际上不是“同步”,而是一种确保变量副本更新的方法任何需要的地方——在 Java 中,一个适当的同步块也可以满足这个要求)。
        • 简而言之,从更多线程访问的变量(即使线程等待彼此的写操作完成,正如 Robert Harvey 提到的)应该在一种强制系统更新任何缓存副本的方法。这是正确的吗?
        • 这真的取决于上下文。一般来说,对共享状态的访问应该是同步的,除了少数极端情况。一个值得注意的失败示例是循环的入口,其中双方都依赖对共享变量的更新来退出。这可能会在多核系统上导致意外的无限循环,其中每个线程在其自己的副本上执行循环。经验丰富的并发开发人员会竭尽全力(防御性副本、不可变对象、准函数式编程等)来完全避免共享状态,从而避免同步的需要。
        【解决方案5】:

        如果我是对的,someValue 必须是 volatile,否则第三步可能不会返回最新值(因为 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) 仅保证如果您在同一个锁上同步的另一个块中,您不会看到不一致的值。同步对共享变量的所有访问通常是个好主意,除非您真的知道自己在做什么。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-11-02
          • 2013-08-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-12-27
          相关资源
          最近更新 更多