彩虹天堂
技术源于生活
posts - 0,  comments - 2,  trackbacks - 0
多线程因素的考虑

在例1中的ClassicSingleton.getInstance()方法由于下面的代码而不是线程安全的:
Java代码 复制代码
  1. 1if(instance == null) {    
  2. 2:    instance = new Singleton();    
  3. 3: }   


如果一个线程在第二行的赋值语句发生之前切换,那么成员变量instance仍然是null,然后另一个线程可能接下来进入到if块中。在这种情况下,两个不同的单例类实例就被创建。不幸的是这种假定很少发生,这样这种假定也很难在测试期间出现(译注:在这可能是作者对很少出现这种情况而导致无法测试从而使人们放松警惕而感到叹惜)。为了演示这个线程轮换,我得重新实现例1中的那个类。例4就是修订后的单例类:
例4.人为安排的方式

Java代码 复制代码
  1. import org.apache.log4j.Logger;    
  2.      
  3. public class Singleton {    
  4.   private static Singleton singleton = null;    
  5.   private static Logger logger = Logger.getRootLogger();    
  6.   private static boolean firstThread = true;    
  7.      
  8.   protected Singleton() {    
  9.     // Exists only to defeat instantiation.    
  10.   }    
  11.   public static Singleton getInstance() {    
  12.      if(singleton == null) {    
  13.         simulateRandomActivity();    
  14.         singleton = new Singleton();    
  15.      }    
  16.      logger.info("created singleton: " + singleton);    
  17.      return singleton;    
  18.   }    
  19.   private static void simulateRandomActivity() {    
  20.      try {    
  21.         if(firstThread) {    
  22.            firstThread = false;    
  23.            logger.info("sleeping...");    
  24.      
  25.            // This nap should give the second thread enough time    
  26.            // to get by the first thread.    
  27.              Thread.currentThread().sleep(50);    
  28.        }    
  29.      }    
  30.      catch(InterruptedException ex) {    
  31.         logger.warn("Sleep interrupted");    
  32.      }    
  33.   }    
  34. }   


除了在这个清单中的单例类强制使用了一个多线程错误处理,例4类似于例1中的单例类。在getInstance()方法第一次被调用时,调用这个方法的线程会休眠50毫秒以便另外的线程也有时间调用getInstance()并创建一个新的单例类实例。当休眠的线程觉醒时,它也会创建一个新的单例类实例,这样我们就有两个单例类实例。尽管例4是人为如此的,但它却模拟了第一个线程调用了getInstance()并在没有完成时被切换的真实情形。
例5测试了例4的单例类:
例5.失败的测试

Java代码 复制代码
  1. import org.apache.log4j.Logger;    
  2. import junit.framework.Assert;    
  3. import junit.framework.TestCase;    
  4.      
  5. public class SingletonTest extends TestCase {    
  6.    private static Logger logger = Logger.getRootLogger();    
  7.    private static Singleton singleton = null;    
  8.      
  9.    public SingletonTest(String name) {    
  10.       super(name);    
  11.    }    
  12.    public void setUp() {    
  13.       singleton = null;    
  14.    }    
  15.    public void testUnique() throws InterruptedException {    
  16.       // Both threads call Singleton.getInstance().    
  17.       Thread threadOne = new Thread(new SingletonTestRunnable()),    
  18.              threadTwo = new Thread(new SingletonTestRunnable());    
  19.      
  20.       threadOne.start();    
  21.       threadTwo.start();    
  22.      
  23.       threadOne.join();    
  24.       threadTwo.join();    
  25.    }    
  26.    private static class SingletonTestRunnable implements Runnable {    
  27.       public void run() {    
  28.          // Get a reference to the singleton.    
  29.          Singleton s = Singleton.getInstance();    
  30.      
  31.          // Protect singleton member variable from    
  32.          // multithreaded access.    
  33.          synchronized(SingletonTest.class) {    
  34.             if(singleton == null// If local reference is null...    
  35.                singleton = s;     // ...set it to the singleton    
  36.          }    
  37.          // Local reference must be equal to the one and    
  38.          // only instance of Singleton; otherwise, we have two    
  39.                   // Singleton instances.    
  40.          Assert.assertEquals(true, s == singleton);    
  41.       }    
  42.    }    
  43. }   


例5的测试案例创建两个线程,然后各自启动,等待完成。这个案例保持了一个对单例类的静态引用,每个线程都会调用Singleton.getInstance()。如果这个静态成员变量没有被设置,那么第一个线程就会将它设为通过调用getInstance()而得到的引用,然后这个静态变量会与一个局部变量比较是否相等。
在这个测试案例运行时会发生一系列的事情:第一个线程调用getInstance(),进入if块,然后休眠;接着,第二个线程也调用getInstance()并且创建了一个单例类的实例。第二个线程会设置这个静态成员变量为它所创建的引用。第二个线程检查这个静态成员变量与一个局部备份的相等性。然后测试通过。当第一个线程觉醒时,它也会创建一个单例类的实例,并且它不会设置那个静态成员变量(因为第二个线程已经设置过了),所以那个静态变量与那个局部变量脱离同步,相等性测试即告失败。例6列出了例5的输出:
例6.例5的输出

Java代码 复制代码
  1. Buildfile: build.xml    
  2. init:    
  3.      [echo] Build 20030414 (14-04-2003 03:06)    
  4. compile:    
  5. run-test-text:    
  6. INFO Thread-1: sleeping...    
  7. INFO Thread-2: created singleton: Singleton@7e5cbd    
  8. INFO Thread-1: created singleton: Singleton@704ebb    
  9. junit.framework.AssertionFailedError: expected: but was:    
  10.    at junit.framework.Assert.fail(Assert.java:47)    
  11.    at junit.framework.Assert.failNotEquals(Assert.java:282)    
  12.    at junit.framework.Assert.assertEquals(Assert.java:64)    
  13.    at junit.framework.Assert.assertEquals(Assert.java:149)    
  14.    at junit.framework.Assert.assertEquals(Assert.java:155)    
  15.    at SingletonTest$SingletonTestRunnable.run(Unknown Source)    
  16.    at java.lang.Thread.run(Thread.java:554)    
  17.      [java] .    
  18.      [java] Time: 0.577    
  19.      
  20.      [java] OK (1 test)   


到现在为止我们已经知道例4不是线程安全的,那就让我们看看如何修正它。


同步

要使例4的单例类为线程安全的很容易----只要像下面一个同步化getInstance()方法:
Java代码 复制代码
  1. public synchronized static Singleton getInstance() {    
  2.    if(singleton == null) {    
  3.       simulateRandomActivity();    
  4.       singleton = new Singleton();    
  5.    }    
  6.    logger.info("created singleton: " + singleton);    
  7.    return singleton;    
  8. }   

在同步化getInstance()方法后,我们就可以得到例5的测试案例返回的下面的结果:
Java代码 复制代码
  1. Buildfile: build.xml    
  2.      
  3. init:    
  4.      [echo] Build 20030414 (14-04-2003 03:15)    
  5.      
  6. compile:    
  7.     [javac] Compiling 2 source files    
  8.      
  9. run-test-text:    
  10. INFO Thread-1: sleeping...    
  11. INFO Thread-1: created singleton: Singleton@ef577d    
  12. INFO Thread-2: created singleton: Singleton@ef577d    
  13.      [java] .    
  14.      [java] Time: 0.513    
  15.      
  16.      [java] OK (1 test)   


这此,这个测试案例工作正常,并且多线程的烦恼也被解决;然而,机敏的读者可能会认识到getInstance()方法只需要在第一次被调用时同步。因为同步的性能开销很昂贵(同步方法比非同步方法能降低到100次左右),或许我们可以引入一种性能改进方法,它只同步单例类的getInstance()方法中的赋值语句。
posted on 2008-05-05 22:09 bcterry 阅读(68) 评论(0)  编辑  收藏

只有注册用户登录后才能发表评论。


网站导航:
 

<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

留言簿

文章档案

搜索

  •  

最新评论