7

我试图弄清楚下面的代码是否存在任何潜在的并发问题。具体来说,与易失性变量相关的可见性问题。Volatile被定义为:这个变量的值永远不会被缓存在线程本地:所有的读写都将直接进入“主内存”

public static void main(String [] args)
{
    Test test = new Test();

    // This will always single threaded
    ExecutorService ex = Executors.newSingleThreadExecutor();

    for (int i=0; i<10; ++i)
        ex.execute(test);
}

private static class Test implements Runnable {
    // non volatile variable in question
    private int state = 0;

    @Override
    public void run() {
        // will we always see updated state value? Will updating state value
        // guarantee future run's see the value?
        if (this.state != -1)
            this.state++;
    }
}

对于上面的单线程执行器

可以使test.state非易失性吗?换句话说,每个连续的 Test.run()(这将顺序发生而不是同时发生,因为再次执行程序是单线程的),总是看到更新的test.state值?如果不是,退出Test.run()是否不会确保在本地对线程所做的任何更改都写回主内存?否则,如果不是在线程退出时,本地所做的更改何时会被写回主内存?

4

6 回答 6

4

只要它只是一个线程,就没有必要让它变得易变。如果你打算使用多个线程,你不仅应该使用 volatile,还应该使用 synchronize。递增数字不是原子操作- 这是一个常见的误解。

public void run() {
    synchronize (this) {
        if (this.state != -1)
            this.state++;
    }
}

除了使用同步之外,您还可以使用AtomicInteger#getAndIncrement()(如果您不需要 if 之前)。

private AtomicInteger state = new AtomicInteger();

public void run() {
    state.getAndIncrement()
}
于 2009-12-14T16:15:33.460 回答
3

本来我是这样想的:

如果任务总是由同一个线程执行,就没有问题。但Excecutor生产者 newSingleThreadExecutor()可能会创建新线程来替换因任何原因而被杀死的线程。无法保证何时创建替换线程或创建它的线程。

如果一个线程执行一些写操作,然后调用start()一个新线程,这些写操作将对新线程可见。但不能保证该规则适用于这种情况。

但不可否认的是正确的:在没有足够障碍的情况下创建正确的ExecutorService以确保可见性实际上是不可能的。我忘记了检测另一个线程的死亡是一种同步关系。用于空闲工作线程的阻塞机制也需要一个屏障。

于 2009-12-14T16:57:59.927 回答
2

是的,它是安全的,即使执行者在中间替换了它的线程。线程开始/终止也是同步点。

http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.4.4

一个简单的例子:

static int state;
static public void main(String... args) {
    state = 0;                   // (1)
    Thread t = new Thread() {
        public void run() {
            state = state + 1;   // (2) 
        }
    };
    t.start();
    t.join();
    System.out.println(state);  // (3)
}

保证 (1)、(2)、(3) 有序且行为符合预期。

对于单线程执行器,“任务保证按顺序执行”,它必须在开始下一个任务之前以某种方式检测一个任务的完成,这必然会正确同步不同run()

于 2009-12-14T18:13:04.943 回答
0

您的代码,特别是这一点

            if (this.state != -1)
                    this.state++;

将需要对状态值进行原子测试,然后在并发上下文中对状态进行增量。因此,即使您的变量是易变的并且涉及多个线程,您也会遇到并发问题。

但是您的设计基于断言始终只有一个 Test 实例,并且该单个实例仅授予单个(相同)线程。(但请注意,单个实例实际上是主线程和执行线程之间的共享状态。)

我认为您需要使这些假设更加明确(例如,在代码中,使用 ThreadLocal 和 ThreadLocal.get())。这是为了防止未来的错误(当其他一些开发人员可能不小心违反设计假设时),并且防止对您正在使用的 Executor 方法的内部实现做出假设,在某些实现中可能只是提供单线程执行器(即在每次执行(可运行)调用中顺序且不一定是相同的线程。

于 2009-12-14T16:31:08.947 回答
0

在这个特定的代码中,状态是非易失性的是非常好的,因为只有一个线程,并且只有那个线程访问该字段。在您拥有的唯一线程中禁用缓存该字段的值只会影响性能。

但是,如果您希望在运行循环的主线程中使用 state 的值,则必须使该字段为 volatile:

    for (int i=0; i<10; ++i) {
            ex.execute(test);
            System.out.println(test.getState());
    }

但是,即使这样也可能无法在 volatile 上正常工作,因为线程之间没有同步。

由于该字段是私有的,因此只有在主线程执行可以访问该字段的方法时才会出现问题。

于 2009-12-14T16:37:23.080 回答
-1

如果您的 ExecutorService 是单线程的,那么就没有共享状态,所以我看不出有什么问题。

Test但是,将您的类的新实例传递给每个调用不是更有意义execute()吗?IE

for (int i=0; i<10; ++i)
    ex.execute(new Test());

这样就不会有任何共享状态。

于 2009-12-14T16:13:41.213 回答