1

据我了解,如果硬件在多处理器系统上支持缓存一致性,那么对共享变量的写入将对其他处理器上运行的线程可见。为了测试这个,我用 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. 即使硬件已经支持它,编译器是否还需要做一些额外的事情来获得缓存一致性?(如启用/禁用)

  4. 我应该默认对所有共享变量使用 Volatile 吗?

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

4

5 回答 5

5

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

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

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

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

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

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

即使硬件已经支持它,编译器是否还需要做一些额外的事情来获得缓存一致性?(如启用/禁用)

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

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 吗?

几乎从不。如果您仅设置一次,则如果您有一个 since 标志,它将起作用。但是,使用锁定通常更有用。

于 2012-10-22T19:47:59.400 回答
3

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

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

这是有效的,因为底层System.out PrintStreamsynchronized导致跨越内存屏障的。内存屏障强制线程和中央内存之间的同步内存,并确保内存操作的顺序。这是PrintStream.println(...)来源:

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

如果缓存一致性是在硬件中完成的,那么 'i=0' 应该对其他线程可见,除非编译器优化了代码

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

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

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

我应该默认对所有共享变量使用 volatile 吗?

如果您希望线程#1 更新一个字段并且线程#2 看到该更新,那么是的,您需要将该字段标记为volatile. Atomic*如果要增加共享变量(++是两个操作) ,通常建议使用类,并且是必需的。

如果您正在执行多项操作(例如遍历共享集合),synchronized则应使用关键字。

于 2012-10-22T20:00:31.477 回答
1

如果线程 2 在线程 1 已经将 i 设置为 0 之后开始运行,则程序将结束。使用synchronized(this)可能会在某种程度上促成这一点,因为在同步块的每个入口处都有一个内存屏障,无论获取的锁是什么(你使用不同的锁,所以不会发生争执)。

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

于 2012-10-22T19:48:42.207 回答
1

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

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

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

于 2012-10-22T20:25:38.977 回答
1

如果预期的行为是线程 2 检测变量的变化并终止,那么肯定需要“Volatile”关键字。它允许thead 能够通过 volatile 变量进行通信。编译器通常会优化从缓存中获取,因为它比从主内存中获取更快。

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

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

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

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

于 2012-10-23T07:10:11.467 回答