2009-12-14 17 views
6

我想弄清楚下面的代码是否存在任何潜在的并发问题。具体而言,可见性问题与易变变量有关。 挥发性定义为:这个变量的值将不会被缓存线程本地:所有读取和写入将直接进入“主内存”并发性,对象可见性

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()(这将会按顺序发生而不是同时执行,因为executor是单线程的),总会看到更新的test.state值?如果没有,不退出Test.run()确保所做的任何更改使得线程在本地被写回主内存?否则,当线程在本地进行更改时,如果线程退出时还没有写回主内存?

+0

你从哪儿弄来该定义。听起来像一个1.5 JMM之前的定义(这是不可实现的)。 –

+0

http://www.javamex.com/tutorials/synchronization_volatile.shtml – Integer

+0

认识到线程完成Test.run()时很重要,线程不会终止,并且任何有关由线程在终止之前刷新到主内存不适用。调用Test.run()的'run()'方法线程只是一个循环,它会阻塞,直到它接收到一个要执行的新任务。当该任务从*它的'run()'方法返回时,该线程将阻塞直到下一个任务;它不会终止(从而刷新其状态)。 – erickson

回答

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()

-1

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

但是,将Test类的新实例传递给​​的每个调用是否更有意义?即

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

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

+1

这并没有任何意义,问题的关键在于使用同一个对象。 – KernelJ

4

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

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

而不是使用同步的,你也可以使用AtomicInteger#getAndIncrement()(如果之前如果你不需要的)。

private AtomicInteger state = new AtomicInteger(); 

public void run() { 
    state.getAndIncrement() 
} 
+0

我只是问一个简单的问题,是否在单线程模式下,其他线程是否会看到状态变化,并回答了这个问题。然而,越来越多的话题,我不明白为什么我需要使用易失性和同步。在我看来,同步就足够了。 – Integer

+0

我只是想让你(和其他人在这个问题上磕磕绊绊)意识到使用volatile来递增一个整数在多线程环境中是不够的。您将始终需要volatile和synchronized两者以保证线程安全。 – sfussenegger

+0

易失性通常不足以解决问题的原因很明显:由于++的非原子性,线程的某些增量可能不会因竞争条件而产生影响。但为什么同步没有波动不足呢?当然,如果你同步读取。 –

0

您的代码,具体地该位

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

将需要状态值的原子测试,然后增量到状态在并发上下文。所以即使你的变量是不稳定的并且涉及多个线程,你也会遇到并发问题。

但是,您的设计基于声明总是只有一个测试实例,,该单个实例仅授予单个(相同)线程。 (但请注意,单实例实际上是主线程和执行程序线程之间的共享状态。)

我认为您需要使这些假设更加明确(例如,在代码中使用ThreadLocal和ThreadLocal 。得到())。这是为了防范未来的错误(当一些其他开发者可能不小心违反了设计假设时),并且防止对关于the Executor method you are using的内部实现的假设,这在某些实现中可能仅仅提供单线程执行程序(即,在execute(runnable)的每个调用中都是顺序的而不一定是相同的线程。

+0

听着,我知道代码不是一件艺术品。我知道有数百万个可以改进的地方。是的,我知道threadlocals,我知道关于同步和原子变量。这不是生产代码。这是代码来说明我的并发可见性和易变性问题。 – Integer

0

状态在此特定代码中是非易失性的,因为只有一个线程,并且只有该线程才能访问该字段,这是完全正确的。禁用在你唯一的线程中缓存这个字段的值只会给性能带来影响。

不过,如果你想使用的状态的值,在其运行循环的主线程,你必须让外地挥发性:

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

然而,即使这样可能无法正常使用挥发性工作,因为线程之间没有同步。

由于该字段是私有的,因此如果主线程执行一个可以访问该字段的方法,则只有一个问题。

3

原来我是这样想的:

如果任务总是由 同一个线程中执行,就不会有 问题。但Excecutor产生的 newSingleThreadExecutor()可能会创建 新线程来替换那些因为任何原因而被杀死的线程。关于什么时候替换 线程将被创建或哪个线程 将创建它,没有 保证。

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

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

+0

“不能保证何时将创建替换线程或哪个线程创建它。”执行者是否在同一个线程或替换线程中运行Test,它是否有所作为?因为我使用的是Test()的同一个实例。我的问题是特定于Test.run()的所有执行是否会看到先前更新的状态值。 – Integer

+2

是的,不管是否使用同一个线程来运行'Test',都会有所不同。如果一个线程保证运行'run()'的所有执行,那么这只是普通的单线程编程,并且在该线程的后续调用中肯定会看到更新。但是,因为任务不使用内存屏障,所以这些更新可能对*不同的线程不可见,包括由执行程序创建的替换线程。 – erickson

+0

谢谢。你完美地回答了我的问题。 – Integer