2010-03-09 78 views
11

如何通过编写一些代码快速证明以下类不是线程安全的(因为它使用Lazy Initialization并且不使用同步)? 换句话说,如果我为了线程安全测试下面的类,我怎么会失败呢?验证以下代码不是线程安全的

public class LazyInitRace { 
    private ExpensiveObject instance = null; 

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

会在构造函数帮助中添加一个'Thread.sleep'吗? – Amarghosh 2010-03-09 16:18:42

+2

您是否*有*使用代码来证明它,或者您能否以其他方式证明它?一个简单的执行图可以用来证明它不安全。 – Herms 2010-03-09 16:20:48

+0

有什么证明?这不是线程安全的。 – ChaosPandion 2010-03-09 16:24:54

回答

1

那么...这个代码的结果将是错误的,你期望的是真实的。

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) { 
     } 
    } 
} 
+0

如果我在构造函数中省略Thread.sleep(),则返回true。如果我运行它几千次,我有时会失败。 – portoalet 2010-03-10 01:13:46

12

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

做出天真的双重检查锁定失败将变得更加困难,请注意......(尽管没有指定volatile作为变量并不安全)。

+1

@Jon Skeet:请原谅我的无知,但我想知道volatile的具体含义? – 2010-03-09 16:44:41

+1

volatile是一个java关键字,用于控制jvm如何处理线程之间的内存访问。 – 2010-03-09 16:51:47

-1

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

  1. 让ExpensiveObject构造完全安全:

    同步ExpensiveObject(){...

  2. 的地方,构造器,如果另一个副本,检查代码的对象存在 - 然后引发异常。

  3. 创建线程安全的方法来清除“实例”的getInstance/clearInstance的可变

  4. 放置顺序代码循环用于多个线程执行,并等待来自(2)

+0

我不认为他是在寻求使代码线程安全的方法,但是为了显示它不是的方式。 – 2010-03-09 16:24:50

+0

@Michael Borgwardt看看(2) - 引发异常是SHOW的不安全。我用过的所有其他同步技巧只是为了避免在现有代码中引入新问题。 – Dewfy 2010-03-09 16:40:56

+0

你的#1无效,它不会编译。 “synchronized”不是构造函数的有效修饰符。所有的构造函数本质上都是线程安全的。除此之外,您提出的解决方案过于复杂......只需使该静态“getInstance”方法同步即可。 – NateS 2010-03-09 21:51:34

14

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

顺便说一句,这没有一个真正构成“证据”。 Formal Verification会,但是非常非常难以完成,即使是相对少量的代码。

+5

+1正确使用“证明”一词 – Seth 2010-03-09 16:28:20

+3

任何证明它失败*证明*它可能会失败,所以在这里我没有看到使用“证明”的任何问题。 – Herms 2010-03-09 16:38:31

+2

同意。反例证明是有效的证明。 – 2010-03-09 16:52:57

0

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

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

你可能想通过一个较大的数目,如果MAX_VALUE的时间太长了自己的喜好来降低终止条件Double.MAX_VALUE/2.0或除法。

3

由于这是Java中,你可以使用thread-weaver库注入停顿或中断到你的代码,并控制多个执行线程。通过这种方式,您可以获得缓慢的构造函数,而不必修改构造函数代码,因为其他人已经(正确)建议。

+0

以前从未听说过那个图书馆。看起来很有趣。 – Herms 2010-03-09 16:54:23

5

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

| 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  |      | 
0

你可以用调试器很容易地证明它。

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

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

相关问题