【问题标题】:Proving the following code not thread safe证明以下代码不是线程安全的
【发布时间】:2011-01-25 12:19:13
【问题描述】:

如何通过编写一些代码快速证明以下类不是线程安全的(因为它使用延迟初始化而不使用同步)? 换句话说,如果我正在测试以下类的线程安全性,我怎么会失败呢?

public class LazyInitRace {
  private ExpensiveObject instance = null;

  public ExpensiveObject getInstance() {
     if (instance == null)
        instance = new ExpensiveObject();
    return instance;
  }
}

【问题讨论】:

  • 会在构造函数帮助中添加Thread.sleep 吗?
  • 用代码来证明,或者你能用其他方式证明吗?一个简单的执行图可以用来证明它是不安全的。
  • 有什么要证明的?这不是线程安全的。
  • 同意 Herms,这似乎是一种简单的案例,您甚至不需要“证明”它
  • 显而易见并不意味着能够证明它没有价值。线程和线程安全可能很复杂,有时能够证明即使是这样简单的案例也很好,如果只是为了帮助教育那些不熟悉并发问题的人。

标签: java multithreading thread-safety


【解决方案1】:

根据定义,除非您控制线程调度程序(您没有控制),否则无法确定性地测试竞争条件。您可以做的最接近的事情是在 getInstance() 方法中添加可配置的延迟,或者编写代码可能出现问题并在循环中运行数千次。

顺便说一句,这些都不是真正的“证据”。 Formal Verification 会,但很难做到,即使是相对少量的代码。

【讨论】:

  • 任何证明它失败的演示证明它可能会失败,所以我真的看不出这里使用“证明”有什么问题。
  • 同意。反例证明是有效的证明。
  • 问题是第一种产生反例的方法需要修改代码,第二种是运气问题。
  • 查看我关于通过调试器调度线程的答案。粗鲁但对简单的情况有效。
【解决方案2】:

你能强制ExpensiveObject 在你的测试中花费很长时间来构建吗?如果是这样,只需从两个不同的线程调用getInstance() 两次,在足够短的时间内,第一个构造函数在第二次调用之前不会完成。您最终将构建两个不同的实例,这就是您应该失败的地方。

请注意,让幼稚的双重检查锁定失败会更加困难...(即使不为变量指定 volatile 是不安全的)。

【讨论】:

  • @Jon Skeet:请原谅我的无知,但我想知道volatile的具体含义?
  • volatile 是一个 java 关键字,用于控制 jvm 如何处理线程之间的内存访问。
【解决方案3】:

这不是使用代码,但这是我如何证明它的示例。这种执行图的标准格式我忘记了,但是意思应该很明显了。

| Thread 1              | Thread 2              |
|-----------------------|-----------------------|
| **start**             |                       |
| getInstance()         |                       |
| if(instance == null)  |                       |
| new ExpensiveObject() |                       |
| **context switch ->** | **start**             |
|                       | getInstance()         |
|                       | if(instance == null)  | //instance hasn't been assigned, so this check doesn't do what you want
|                       | new ExpensiveObject() |
| **start**             | **<- context switch** |
| instance = result     |                       |
| **context switch ->** | **start**             |
|                       | instance = result     |
|                       | return instance       |
| **start**             | **<- context switch** |
| return instance       |                       |

【讨论】:

    【解决方案4】:

    由于这是 Java,您可以使用 thread-weaver 库将暂停或中断注入您的代码并控制多个执行线程。这样你就可以得到一个缓慢的ExpensiveObject 构造函数,而无需像其他人(正确)建议的那样修改构造函数代码。

    【讨论】:

    • 以前从未听说过那个库。看起来很有趣。
    【解决方案5】:

    嗯...这段代码的结果将是假的,你期望的是真。

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    public class LazyInitRace {
    
        public class ExpensiveObject {
            public ExpensiveObject() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        }
    
        private ExpensiveObject instance = null;
    
        public ExpensiveObject getInstance() {
            if (instance == null)
                instance = new ExpensiveObject();
            return instance;
        }
    
        public static void main(String[] args) {
            final LazyInitRace lazyInitRace = new LazyInitRace();
    
            FutureTask<ExpensiveObject> target1 = new FutureTask<ExpensiveObject>(
                    new Callable<ExpensiveObject>() {
    
                        @Override
                        public ExpensiveObject call() throws Exception {
                            return lazyInitRace.getInstance();
                        }
                    });
            new Thread(target1).start();
    
            FutureTask<ExpensiveObject> target2 = new FutureTask<ExpensiveObject>(
                    new Callable<ExpensiveObject>() {
    
                        @Override
                        public ExpensiveObject call() throws Exception {
                            return lazyInitRace.getInstance();
                        }
                    });
            new Thread(target2).start();
    
            try {
                System.out.println(target1.get() == target2.get());
            } catch (InterruptedException e) {
            } catch (ExecutionException e) {
            }
        }
    }
    

    【讨论】:

    • 如果我在构造函数中省略了 Thread.sleep(),那么它返回 true。也许如果我运行它数千次,我有时会出错。
    【解决方案6】:

    在构造函数中放一个很长的计算:

    public ExpensiveObject()
    {
        for(double i = 0.0; i < Double.MAX_VALUE; ++i)
        {
            Math.pow(2.0,i);
        }
    }
    

    如果您不喜欢 MAX_VALUE 花费的时间过长,您可能希望将终止条件减少到 Double.MAX_VALUE/2.0 或除以更大的数字。

    【讨论】:

      【解决方案7】:

      您可以使用调试器轻松证明这一点。

      1. 编写一个程序调用 getInstance() 在两个单独的 线程。
      2. 在构造上设置断点 昂贵的对象。确保 调试器只会暂停 线程,而不是 VM。
      3. 然后当第一个线程停止时 断点,让它暂停。
      4. 当第二个线程停止时,您只需继续。
      5. 如果检查结果 getInstance() 调用两个线程, 他们将指的是不同的 实例。

      这样做的好处是,您实际上不需要 ExpensiveObject,任何 Object 实际上都会产生相同的结果。您只是使用调试器来安排特定代码行的执行,从而创建确定性结果。

      【讨论】:

        【解决方案8】:

        嗯,它不是线程安全的。 线程安全的证明是随机的,但相当简单:

        1. 使 ExpensiveObject 构造函数完全安全:

          同步的 ExpensiveObject(){ ...

        2. 将检查是否存在另一个对象副本的构造函数代码放置 - 然后引发异常。

        3. 创建线程安全方法以清除“实例”变量

        4. 将 getInstance/clearInstance 的顺序代码放置到循环中以供多线程执行并等待 (2) 中的异常

        【讨论】:

        • 我认为他不是在寻求使代码线程安全的方法,而是寻求一种显示它不是的方法。
        • @Michael Borgwardt 看看 (2) - 引发异常是不安全的 SHOW。我使用的所有其他同步技巧只是为了避免在现有代码中引入新问题。
        • 你的#1 无效,它不会编译。 “同步”不是构造函数的有效修饰符。所有构造函数本质上都是线程安全的。除此之外,您提出的解决方案过于复杂......只需同步该静态“getInstance”方法即可。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-07-11
        • 1970-01-01
        • 2010-10-30
        • 2019-01-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多