并发:缓存一致性问题还是编译器优化?

时间:2012-10-22 19:34:49

标签: java multithreading concurrency pthreads javac

根据我的理解,如果硬件支持多处理器系统上的高速缓存一致性,则对其他处理器上运行的线程可以看到对共享变量的写入。为了测试这个,我用Java和pThreads编写了一个简单的程序来测试这个

public class mainTest {

    public static int i=1, j = 0;
    public static void main(String[] args) {

    /*
     * Thread1: Sleeps for 30ms and then sets i to 1
     */
    (new Thread(){
        public void run(){
            synchronized (this) {
                try{
                       Thread.sleep(30);
                       System.out.println("Thread1: j=" + mainTest.j);
                       mainTest.i=0;
                   }catch(Exception e){
                       throw new RuntimeException("Thread1 Error");
                }
            }
        }
    }).start();

    /*
     * Thread2: Loops until i=1 and then exits.
     */
    (new Thread(){
        public void run(){
            synchronized (this) {
                while(mainTest.i==1){
                    //System.out.println("Thread2: i = " + i); Comment1
                    mainTest.j++;
                }
                System.out.println("\nThread2: i!=1, j=" + j);
            }
        }
    }).start();

   /*
    *  Sleep the main thread for 30 seconds, instead of using join. 
    */
    Thread.sleep(30000);
    }
}




/* pThreads */

#include<stdio.h>
#include<pthread.h>
#include<assert.h>
#include<time.h>

int i = 1, j = 0;

void * threadFunc1(void * args) {
    sleep(1);
    printf("Thread1: j = %d\n",j);
    i = 0;
}

void * threadFunc2(void * args) {
while(i == 1) {
        //printf("Thread2: i = %d\n", i);
        j++;
    }
}

int main() {
    pthread_t t1, t2;
    int res;
    printf("Main: creating threads\n");

    res = pthread_create(&t1, NULL, threadFunc1, "Thread1"); assert(res==0);
    res = pthread_create(&t2, NULL, threadFunc2, "Thread2"); assert(res==0);

    res = pthread_join(t1,NULL); assert(res==0);
    res = pthread_join(t2,NULL); assert(res==0);

    printf("i = %d\n", i);
    printf("Main: End\n");
    return 0;
}    

我注意到pThread程序总是结束。 (我为thread1测试了不同的睡眠时间)。然而,Java程序只结束了几次;大部分时间都没有结束。 如果我在java程序中取消注释Comment1,那么它会一直结束。此外,如果我使用volatile,那么在所有情况下它都会以java结尾。

所以我的困惑是,

  1. 如果在硬件中完成高速缓存一致性,那么“i = 0”应该对其他线程可见,除非 编译器优化了代码。但是如果编译器优化了代码,那么我不明白为什么线程有时会结束而有时不会结束。另外,添加System.out.println似乎会改变行为。

  2. 任何人都可以看到Java所做的编译器优化(C编译器没有这样做),这会导致这种行为吗?

  3. 编译器必须做些什么,即使硬件已经支持它,也能获得Cache一致性? (如启用/禁用)

  4. 默认情况下,我是否应该为所有共享变量使用Volatile?

  5. 我错过了什么吗?欢迎提出任何其他意见。

5 个答案:

答案 0 :(得分:5)

  

如果在硬件中完成高速缓存一致性,那么除非编译器对代码进行优化,否则“i = 0”应该对其他线程可见。但是如果编译器优化了代码,那么我不明白为什么线程有时会结束而有时不会结束。另外,添加System.out.println似乎会改变行为。

注意:javac旁边没有优化,所以不要考虑静态优化。

您正在锁定与您正在修改的对象无关的不同对象。由于您正在修改的字段不是volatile,因此JVM优化器可以根据其选择自由地动态优化它,无论您的硬件可以提供哪种支持。

由于这是动态的,它可能会也可能不会优化您在该线程中不会更改的字段的读取。

  

任何人都可以看到Java所做的编译器优化(C编译器没有这样做),这会导致这种行为吗?

优化很可能是将读取缓存在寄存器中或完全消除代码。此优化通常需要大约10-30毫秒,因此您要测试在程序完成之前是否已发生此优化。

  

编译器必须做些什么,即使硬件已经支持它,也能获得Cache一致性? (如启用/禁用)

您必须正确使用该模型,忘记编译器将优化代码的想法,并理想地使用并发库在线程之间传递工作。

public static void main(String... args) {
    final AtomicBoolean flag = new AtomicBoolean(true);
    /*
    * Thread1: Sleeps for 30ms and then sets i to 1
    */
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(30);
                System.out.println("Thread1: flag=" + flag);
                flag.set(false);
            } catch (Exception e) {
                throw new RuntimeException("Thread1 Error");
            }
        }
    }).start();

    /*
    * Thread2: Loops until flag is false and then exits.
    */
    new Thread(new Runnable() {
        @Override
        public void run() {
            long j = 0;
            while (flag.get())
                j++;
            System.out.println("\nThread2: flag=" + flag + ", j=" + j);
        }
    }).start();
}

打印

Thread1: flag=true

Thread2: flag=false, j=39661265
  

默认情况下,我是否应该为所有共享变量使用Volatile?

几乎没有。如果你只设置了一次,那么它会有效。但是,使用锁定通常更有用。

答案 1 :(得分:3)

您的具体问题是,第一个线程将i设置为0后,第二个线程需要同步内存。因为两个线程都在this上同步,正如@Peter和@Marko指出的那样是不同的对象。在第一个线程集while之前,第二个线程可以进入i = 0循环。 <{1}}循环中没有额外的内存屏障,因此该字段永远不会更新。

  

如果我在java程序中取消注释Comment1,那么它会一直结束。

这样做是因为基础while System.outPrintStream,导致memory-barrier被越过。synchronized。内存屏障强制线程和中央内存之间的同步内存,并确保内存操作的顺序。这是PrintStream.println(...)来源:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}
  

如果在硬件中完成缓存一致性,那么除非编译器对代码进行优化,否则“i = 0”应该对其他线程可见

您必须记住,每个处理器都有一些寄存器和大量的每处理器高速缓存。它是缓存的内存,这是主要的问题,而不是编译器优化。

  

任何人都可以看到Java所做的编译器优化(C编译器没有这样做),这会导致这种行为吗?

使用缓存内存和内存操作重新排序都是重要的性能优化。处理器可以自由地改变操作顺序以改进流水线操作,除非超过内存屏障,否则它们不会同步它们的脏页面。这意味着线程可以使用本地高速内存异步运行,以[显着]提高性能。 Java内存模型允许这样做,并且与pthreads相比要复杂得多。

  

默认情况下,我应该为所有共享变量使用volatile吗?

如果您希望线程#1更新字段而线程#2看到该更新然后是,则需要将该字段标记为volatile。通常建议使用Atomic*类,如果要增加共享变量(++是两个操作),则需要使用{。}}。

如果您正在进行多项操作(例如在共享集合中进行迭代),则应使用synchronized关键字。

答案 2 :(得分:1)

如果在线程1已经将i设置为0之后线程2开始运行,程序将结束。使用synchronized(this)可能会对此有所贡献,因为无论锁定在每个条目的同步块中都存在内存屏障获得(你使用不同的锁,所以不会发生争论)。

除此之外,在代码获得JITted的时刻与Thread 1写入0的那一刻之间可能存在其他复杂的交互,因为这会改变优化级别。优化代码通常只从全局变量中读取一次,并将值缓存在寄存器或类似的线程本地位置。

答案 3 :(得分:1)

缓存一致性是硬件级别的功能。如何操作变量映射到CPU指令并间接操作硬件是语言/运行时功能。

换句话说,设置变量不一定会转换为写入该变量内存的CPU指令。编译器(离线或JIT)可以使用其他信息来确定它不需要写入内存。

话虽如此,大多数支持并发的语言都有额外的语法告诉编译器您正在使用的数据是用于并发访问的。对于许多人(如Java),它是选择加入。

答案 4 :(得分:1)

如果线程2的预期行为是检测变量的变化并终止,则必须使用“Volatile”关键字。它允许thead能够通过volatile变量进行通信。编译器通常优化从缓存中获取,因为与从主内存中获取相比,它更快。

看看这个很棒的帖子,它会给你答案: http://jeremymanson.blogspot.sg/2008/11/what-volatile-means-in-java.html

我相信在这种情况下,它与缓存一致性无关。如前所述,它是一种计算机体系结构功能,对c / java程序应该是透明的。 如果没有指定volatile,则行为是未定义的,这就是为什么有时另一个线程可以获得值更改,有时它不能。

volatile在C和java上下文中有不同的含义。 http://en.wikipedia.org/wiki/Volatile_variable

根据您的C编译器,程序可能会得到优化并具有与java程序相同的效果。所以总是建议使用volatile关键字。