代码之家  ›  专栏  ›  技术社区  ›  Lydon Ch

证明以下代码不是线程安全的

  •  12
  • Lydon Ch  · 技术社区  · 15 年前

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

    public class LazyInitRace {
      private ExpensiveObject instance = null;
    
      public ExpensiveObject getInstance() {
         if (instance == null)
            instance = new ExpensiveObject();
        return instance;
      }
    }
    
    8 回复  |  直到 15 年前
        1
  •  2
  •   nanda    15 年前

    好。。。这段代码的结果将是假的,而您期望的是真的。

    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) {
            }
        }
    }
    
        2
  •  15
  •   Michael Borgwardt    15 年前

    根据定义,除非您控制线程调度程序(您没有),否则无法确定地测试竞争条件。您可以做的最接近的事情是在 getInstance() 方法,或在问题所在编写代码 可以 在一个循环中显化并运行它数千次。

    顺便说一句,这些都不构成“证据”。 Formal Verification 即使对于相对少量的代码,也很难做到。

        3
  •  13
  •   Jon Skeet    15 年前

    你能强迫吗 ExpensiveObject 要花很长时间在测试中构建吗?如果是,请打电话 getInstance() 在足够短的时间内从两个不同的线程中执行两次,第一个构造函数在第二次调用之前无法完成。最后将构建两个不同的实例,这就是您应该失败的地方。

    让天真的双重检查锁定失败会更难,记住…(即使没有说明是不安全的 volatile 对于变量)。

        4
  •  5
  •   Herms    15 年前

    这不是使用代码,但这里有一个例子说明了我将如何证明它。我忘记了这样的执行图的标准格式,但其含义应该足够明显。

    | 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       |                       |
    
        5
  •  3
  •   mpm    15 年前

    由于这是Java,所以您可以使用 thread-weaver 用于向代码中注入暂停或中断并控制多个执行线程的库。这样你可以慢慢来 ExpensiveObject 不需要修改构造函数代码的构造函数,正如其他人(正确地)建议的那样。

        6
  •  0
  •   Kiril    15 年前

    在构造函数中进行非常长的计算:

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

    您可能希望将终止条件减少到 Double.MAX_VALUE/2.0 或者如果 MAX_VALUE 花的时间太长了。

        7
  •  0
  •   Robin    15 年前

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

    1. 编写一个调用 两个独立的getInstance()。 线程。
    2. 在结构上设置断点 昂贵的物品。确保 调试器将只挂起 线程,而不是虚拟机。
    3. 当第一个线程停止时 断点,保持其挂起。
    4. 当第二个线程停止时,您只需继续。
    5. 如果您检查 两个线程的getInstance()调用, 它们指的是不同的 实例。

    这样做的好处是,你实际上不需要一个昂贵的对象,任何对象实际上都会产生相同的结果。您只需使用调试器来计划特定代码行的执行,从而创建确定性结果。

        8
  •  -1
  •   Dewfy    15 年前

    嗯,这不是线程安全的。 螺纹安全性的检验是随机的,但相当简单:

    1. 使昂贵的对象构造完全安全:

      synchronized expensive object()…

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

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

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