【问题标题】:Thread value not cached by threads even without volatile?即使没有易失性,线程值也不会被线程缓存?
【发布时间】:2013-07-18 22:39:58
【问题描述】:
class Counter
{
    public int i=0;
    public void increment()
    {
        i++;
        System.out.println("i is "+i);
        System.out.println("i/=2 executing");
        i=i+22;
        System.out.println("i is (after i+22) "+i);
        System.out.println("i+=1 executing");
        i++;
        System.out.println("i is (after i++) "+i);
    }
    public void decrement()
    {
        i--;
        System.out.println("i is "+i);
        System.out.println("i*=2 executing");
        i=i*2;
        System.out.println("i is after i*2"+i);
        System.out.println("i-=1 executing");
        i=i-1;
        System.out.println("i is after i-1 "+i);
    }
    public int value()
    {
        return i;
    } }

class ThreadA
{
    public ThreadA(final Counter c)
    {
        new Thread(new Runnable(){
            public void run()
            {
                System.out.println("Thread A trying to increment");
                c.increment();
                System.out.println("Increment completed "+c.i);
            }
        }).start();
    }
}
class ThreadB
{
    public ThreadB(final Counter c)
    {
        new Thread(new Runnable(){
            public void run()
            {
                System.out.println("Thread B trying to decrement");
                c.decrement();
                System.out.println("Decrement completed "+c.i);
            }
        }).start();
    }
}
class ThreadInterference
{
    public static void main(String args[]) throws Exception
    {
        Counter c=new Counter();
        new ThreadA(c);
        new ThreadB(c); 
    }
}

在上面的代码中,ThreadA 首先获得了对 Counter 对象的访问权,并在执行一些额外操作的同时增加了该值。 ThreadA 第一次没有缓存值 i。但是在执行 i++ (在第一行)之后,它将缓存该值。后来值更新并得到 24。根据程序,由于变量 i 不是易失性的,因此更改将在 ThreadA 的本地缓存中完成,

现在当 ThreadB 访问 decrement() 方法时,i 的值由 ThreadA 更新,即 24。这怎么可能?

【问题讨论】:

  • 在 Intel-32 等设备上,缓存一致性是在硬件中实现的,尽管编译器重新排序仍然会导致“问题”。值被缓存在 CPU 寄存器中的剩余问题可能只是琐碎的测试用例的问题。调用几乎任何东西,尤其是像 'println' 这样的大型东西,肯定会导致所有可能的寄存器被压入堆栈,因此至少被压入 L1 缓存。

标签: java multithreading volatile


【解决方案1】:

假设线程不会看到其他线程对共享数据所做的每次更新,这与假设所有线程会立即看到彼此的更新一样不合适。

重要的是要考虑到看不到更新的可能性 - 不要依赖它。

除了看不到来自其他线程的更新之外,还有另一个问题,请注意 - 您的所有操作都在“读取、修改、写入”的意义上进行......如果另一个线程在您阅读后修改了该值,您'基本上会忽略它。

例如,假设当我们到达这一行时i 是 5:

i = i * 2;

...但是到了一半,另一个线程将其修改为 4。

那行可以认为是:

int tmp = i;
tmp = tmp * 2;
i = tmp;

如果第二个线程在“扩展”版本的第一行之后将i 更改为4,那么即使i 是易失性的,4 的写入仍将有效丢失-因为到那时,tmp是5,会加倍到10,再写出10。

【讨论】:

  • 那么你的意思是,如果数据是共享数据,那么一个线程可以看到其他线程的更新。那么在共享数据的情况下不会进行缓存?
  • @GowthamGutha:不是它不会——而是它可能不会。 Java 内存模型指定了必须在哪里看到更新...但它允许实现在看到更新时在这些规则内准确发生变化。
  • 根据Java内存模型,线程什么时候缓存一个值,在哪里看到更新?
【解决方案2】:

JLS 8.3.1.4中指定:

Java 编程语言允许线程访问共享 变量(第 17.1 节)。通常,为了确保共享变量是 一致且可靠地更新,线程应确保它具有 通过获得一个锁来独占使用这些变量, 传统上,对那些共享变量强制互斥........一个字段可能是 声明为 volatile,在这种情况下,Java 内存模型确保 所有线程都看到变量的一致值

虽然并非总是如此,但仍有可能线程之间的共享值不能一致且可靠地更新,这将导致程序的一些不可预测的结果。在下面给出的代码中

class Test {
    static int i = 0, j = 0;
    static void one() { i++; j++; }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

如果,一个线程重复调用方法一(但总共不超过 Integer.MAX_VALUE 次),而另一个线程重复调用方法二,那么方法二偶尔会打印出大于i,因为该示例不包含同步,并且 i 和 j 的共享值可能会乱序更新。
但是,如果您将 ij 声明为 volatile ,这允许方法一和方法二同时执行,但保证访问 ij 的共享值的次数完全相同,并且以完全相同的顺序,因为它们似乎在每个线程执行程序文本期间发生。因此,j 的共享值永远不会大于i 的共享值,因为在更新 j 之前,对 i 的每次更新都必须反映在 i 的共享值中

【讨论】:

    【解决方案3】:

    现在我知道公共对象(由多个线程共享的对象)不会被这些线程缓存。由于对象是常见的,Java 内存模型足够智能,可以识别线程缓存的常见对象可能会产生令人惊讶的结果。

    【讨论】:

      【解决方案4】:

      这怎么可能?

      因为 JLS 中没有任何地方说值必须缓存在线程中。

      这就是规范所说的:

      如果您有一个非易失性变量x,并且它由线程T1 更新,则不能保证T2 可以观察到x 的变化T1。保证T2 看到T1 更改的唯一方法是使用happens-before 关系。

      在某些情况下,Java 的某些实现会在线程中缓存非易失性变量。换句话说,您不能依赖缓存的非易失性变量。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-05-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-10-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多