【问题标题】:Concurrency, object visibility并发,对象可见性
【发布时间】:2010-12-26 11:38:52
【问题描述】:

我试图弄清楚下面的代码是否存在任何潜在的并发问题。具体来说,与易失性变量相关的可见性问题。 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() 确保本地线程所做的任何更改都被写回主内存?否则,如果不是在线程退出时,本地所做的更改何时会被写回主内存?

【问题讨论】:

  • 你从哪里得到这个定义的。听起来像是 1.5 之前的 JMM 定义(无法实现)。
  • 重要的是要意识到当线程完成Test.run() 时,线程不会终止,并且关于线程在终止之前写入的值被刷新到主内存的任何保证都不适用。调用Test.run()run() 方法线程只是一个循环,它会阻塞直到它接收到要执行的新任务。当该任务从 its run() 方法返回时,线程阻塞直到下一个任务;它不会终止(从而刷新其状态)。
  • 这个 q/a 的选民是错误的。由于线程开始/结束的发生之前的关系,代码是安全的。请参阅下面的回复。
  • 你知道吗?在 10 个线程总共调用 this.state++ 1000 次后,this.state 的值可能小于 1000?我不确定您是否只是没有询问此并发问题,还是没有意识到。

标签: java concurrency multithreading volatile


【解决方案1】:

只要它只是一个线程,就没有必要让它变得易变。如果你要使用多个线程,你不仅应该使用 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()
}

【讨论】:

  • 我只是问了一个简单的问题,即在单线程模式下其他线程是否会看到状态更改,而您回答了这个问题。但是离开主题,我不明白为什么我需要使用 volatile 和同步。在我看来,同步就足够了。
  • 我只是想让您(以及其他遇到这个问题的人)意识到在多线程环境中使用 volatile 递增整数是不够的。您将始终需要 volatile 和 synchronized 两者都是线程安全的。
  • 为什么 volatile 经常不够用的原因我很清楚:因为 ++ 的非原子性,一些线程的增量可能由于竞争条件而没有影响。但是为什么没有 volatile 的同步是不够的呢?当然,如果您也同步读取。
  • @hstoerr 没有 volatile 同步就足够了,但使用 volatile(足够)的开销更少(参见j.mp/7RrAzi)。尽管如此,无论性能如何,我总是将两者结合使用:保存总比后悔好。如果您正在处理 64 位类型(long、double),其中甚至不能保证读取和写入是原子的,对非同步字段的并发访问可能会特别成问题:它们可能被拆分为两个 32 位操作。因此,人们甚至可能会读取两种完全不同的值的混合体。
【解决方案2】:

原来我是这样想的:

如果任务总是由 相同的线程,不会有 问题。但是Excecutor出品 newSingleThreadExecutor() 可以创建 新线程来替换那些 因任何原因被杀。没有 保证何时更换 将创建线程或创建哪个线程 将创建它。

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

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

【讨论】:

  • "无法保证何时创建替换线程或创建它的线程。" executor 是在同一个线程还是在替换线程中运行 Test,有区别吗?因为我使用的是同一个 Test() 实例。我的问题是针对 Test.run() 的所有执行是否都会看到先前更新的 State 值。
  • 是的,是否使用同一个线程来运行Test 会产生很大的不同。如果保证单个线程运行run() 的所有执行,那么它只是正常的单线程编程,并且在该线程的后续调用中肯定会看到更新。但是,由于任务不使用内存屏障,这些更新可能对不同的线程不可见,包括由执行程序创建的替换线程。
  • 谢谢。你完美地回答了我的问题。
  • 很抱歉这个答案不正确,请参阅下面的回复。您无法提供满足其 javadoc 的 newSingleThreadExecutor() 的实现,但无法同步不同的 run()。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

    您的代码,特别是这一位

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

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

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

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

    【讨论】:

    • 听着,我知道代码不是艺术品。我知道可以进行数以百万计的改进。是的,我知道线程局部变量,我知道同步变量和原子变量。这不是生产代码。编写代码来说明我的并发可见性和易失性问题。
    【解决方案5】:

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

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

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

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

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

    【讨论】:

      【解决方案6】:

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

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

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

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

      【讨论】:

      • 这没有任何意义,问题的关键在于使用了同一个对象。
      猜你喜欢
      • 2016-12-23
      • 2011-10-12
      • 2011-02-23
      • 1970-01-01
      • 2012-03-11
      • 2021-02-27
      • 2011-08-01
      • 2014-08-15
      • 1970-01-01
      相关资源
      最近更新 更多