mock有两种,一种是静态的,一种是动态的。静态的就是在写测试代码之前根据需要打桩的类生成另外一个类,这个类就是mock object。动态的就是mock object是在测试代码运行的时候才生成的。所以很明显,动态打桩比静态打桩要方便地多。本章就是介绍动态打桩的工具。
早期的动态mock工具只能够mock接口,而不能够mock类;现在的mock工具无论是mock接口还是类都能够轻松完成了。
easymock就是其中的佼佼者,easymock现在已经是2.2版本了,本文使用的是easymock1.2_Java1.5版本。使用 easymock能够轻松的mock任何接口,但如果想mock类,那还需要另外一个lib,就是easymockclassextension,使用了这个工具,你就能轻松地mock你要的任何类了。本文使用的是easymockclassextension1.2,使用这个库必须要cglib这个 jar包,而cglib又需要asm的jar包,所以要搭建好环境还得花些时间,不过当你把环境搞好之后,你便会发现物超所值,从此以后打桩无难事了。 cglib和asm的版本一定要适配,要不然不能正常工作,幸好cglib的网站已经提供了cglib和asm的绑定包,本文所用的是cglib- nodep-2.2_beta1版本。
本章先对如何对接口进行打桩示例,下一章再对如何对接口进行打桩示例。
先看欲打桩接口代码:
/** *//************************************************************
* Project Name: lhjTest
* File Name : CoolInterface.java
* File Desc : CoolInterface.java
* Author : Administrator
* Create : 2007-3-25
* Modify:
***********************************************************/
package org.lhj.cool.junit;
/** *//**
* @author Administrator
*/
public interface CoolInterface
...{
String reverseString(String inputStr) throws NullPointerException;
}
该接口很简单,只要一个reverString的方法。
下面看看欲测试类(使用了CoolInterface)的代码:
/** *//************************************************************
* Project Name: lhjTest
* File Name : UseCoolInterface.java
* File Desc : UseCoolInterface.java
* Author : Administrator
* Create : 2007-3-25
* Modify:
***********************************************************/
package org.lhj.cool.mock;
import org.lhj.cool.junit.CoolInterface;
/** *//**
* @author Administrator
*/
public class UseCoolInterface
...{
private CoolInterface cool;
public void setCoolInterface(CoolInterface cool)
...{
this.cool = cool;
}
public String reverseString(String inputStr) throws NullPointerException
...{
return cool.reverseString(inputStr);
}
}
这个类也很简单,相当于一个adapter。
下面看看如何使用easymock进行接口的打桩:
/** *//************************************************************
* Project Name: lhjTest
* File Name : UseCoolInterfaceJTest.java
* File Desc : UseCoolInterfaceJTest.java
* Author : Administrator
* Create : 2007-3-25
* Modify:
***********************************************************/
package org.lhj.cool.mock;
import junit.framework.TestCase;
import org.easymock.MockControl;
import org.lhj.cool.junit.CoolInterface;
import org.lhj.cool.mock.UseCoolInterface;
/** *//**
* @author Administrator
*/
public class UseCoolInterfaceJTest extends TestCase
...{
private UseCoolInterface testInterface;
private MockControl ctrl;
private CoolInterface cool;
/**//* (non-Javadoc)
* @see junit.framework.TestCase#setUp()
*/
protected void setUp() throws Exception
...{
super.setUp();
ctrl = MockControl.createControl(CoolInterface.class);
cool = (CoolInterface) ctrl.getMock();
testInterface = new UseCoolInterface();
testInterface.setCoolInterface(cool);
}
/**//* (non-Javadoc)
* @see junit.framework.TestCase#tearDown()
*/
protected void tearDown() throws Exception
...{
super.tearDown();
}
/** *//**
* Test method for {@link org.lhj.cool.mock.UseCoolInterface#reverseString(java.lang.String)}.
*/
public void testReverseString()
...{
ctrl.expectAndThrow(cool.reverseString(null), new NullPointerException());
ctrl.expectAndReturn(cool.reverseString(""), null);
ctrl.expectAndReturn(cool.reverseString("AAaa"), "aaAA");
ctrl.expectAndReturn(cool.reverseString("aaAA"), "AAaa");
ctrl.replay();
try
...{
testInterface.reverseString(null);
fail();
}
catch (NullPointerException e)
...{
//donothing
}
assertNull(testInterface.reverseString(""));
assertEquals("aaAA", testInterface.reverseString("AAaa"));
assertEquals("AAaa", testInterface.reverseString("aaAA"));
ctrl.verify();
}
}
从上面可以看出使用easymock的步骤:
1、在setUp时创建MockControl,它是控制mock object行为的类,参数传入欲打桩的接口的class。
2、从mockControl中getmock,返回mock object转化为本类型对象。
3、创建欲测试类并将mock object传进去。
以上几步代码为:
ctrl = MockControl.createControl(CoolInterface.class);
cool = (CoolInterface) ctrl.getMock();
testInterface = new UseCoolInterface();
testInterface.setCoolInterface(cool);
4、使用mockControl对mock object进行设置,这一步又称为训练,代码为
ctrl.expectAndThrow(cool.reverseString(null), new NullPointerException());
ctrl.expectAndReturn(cool.reverseString(""), null);
ctrl.expectAndReturn(cool.reverseString("AAaa"), "aaAA");
ctrl.expectAndReturn(cool.reverseString("aaAA"), "AAaa");
第一行意思是告诉mockControl,mock object的reverseString方法会被调用,传入的参数是null,并会抛出异常;第二行意思是告诉mockControl,mock object的reverseString方法会被调用,传入的参数是"",并返回null;其他行类似。
5、训练完之后可以接受检查了,代码为:
ctrl.replay();
6、现在可以进行测试了,以下是测试代码:
try
...{
testInterface.reverseString(null);
fail();
}
catch (NullPointerException e)
...{
//donothing
}
assertNull(testInterface.reverseString(""));
assertEquals("aaAA", testInterface.reverseString("AAaa"));
assertEquals("AAaa", testInterface.reverseString("aaAA"));
可以看出,这跟一般的JUnit语句是一样的。
7、检验测试结果
ctrl.verify();
mockControl会验证mockObject是否按照训练的情况参与了测试,即训练时的方法是否被调用了,传入的参数对不对。
以上示例只是easymock的一部分功能,除此之外,easymock还能测试mockObject的方法是否按照顺序调用了,调用的次数对不对,传入的参数是否在给定的范围之内,另外还可以改变参数对象的内容。
除了easymock可以mock接口外,像mock objects,Jmock,mock Creator都可以实现此类功能。其中mock Creator还可以mock class,但是可惜mock creator是静态打桩的,实现起来有额外的工作。我在公司里用的是jdk1.4,无法用easymockclassextension1.2,所以使用了easymock加mock creator,幸好mock creator有eclipse插件,生成mock class也比较方便。所以如果你使用的是JDK1。4,用easymock加mock creator绝对是最佳组合;如果你使用的是JDK1.5,那么使用easymock加easymockclassextension会使你工作更轻松。
很多时候,我们要写一些单元测试来测试我们程序是否能正确触发异常。
比如下面的例子中,我们就写了一个test case来测试一个Email验证类EmailAddrValidator,这个类有一个doValidate(email)方法可以验证email是否合法,如果不合法则会抛出ValidationException异常。因此我们写了两个方法来进行单元测试,前一个方法testDoValidate用来测试正常值,后一个方法testDoValidateException用来测试对错误的email格式是否能正确触发异常。
这个例子的关键是方法testDoValidateException(String email) 。
import junit.framework.TestCase;
public class TestEmailAddrValidator extends TestCase {
EmailAddrValidator validator = new EmailAddrValidator();
public void testDoValidate() throws ValidationException {
validator.doValidate("
[email protected]", null);
validator.doValidate("
[email protected]", null);
validator.doValidate("
[email protected]", null);
validator.doValidate("glchen.gang@163_tom.com", null);
}
public void testDoValidateException() {
testDoValidateException("@b.c");
testDoValidateException("
[email protected]");
testDoValidateException("a@b.");
testDoValidateException("@.c");
testDoValidateException("@...");
testDoValidateException(" ");
testDoValidateException(null);
}
private void testDoValidateException(String email) {
try {
validator.doValidate(email, null);
fail("末抛出异常");
} catch (ValidationException e) {
assertTrue(true);
}
}
}
-----------------------------------------
注:在这里Locale 参数并没有用到。
import java.util.Locale;
import com.hygensoft.common.configure.ConfigureObject;
public class EmailAddrValidator{
protected static final String ERROR_CODE_INVALID_EMAIL_ADDR = "INVALID_EMAIL_ADDR";
protected static final String ERROR_CODE_INVALID_INPUT = "INVALID_INPUT_OBJECT";
public Object doValidate(Object input, Locale locale) throws ValidationException {
if (!(input instanceof String)) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
String inputStr = (String) input;
int idx = inputStr.indexOf('@');
if (idx == -1 || idx == 0) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
int idx2 = inputStr.indexOf('.', idx);
if (idx2 == -1 || idx2 == idx + 1) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
if (inputStr.endsWith(".")) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
return input;
}
/* (non-Javadoc)
* @see com.hygensoft.common.configure.Configurable#initialize(com.hygensoft.common.configure.ConfigureObject)
*/
public void initialize(ConfigureObject conf) {}
}
比如下面的例子中,我们就写了一个test case来测试一个Email验证类EmailAddrValidator,这个类有一个doValidate(email)方法可以验证email是否合法,如果不合法则会抛出ValidationException异常。因此我们写了两个方法来进行单元测试,前一个方法testDoValidate用来测试正常值,后一个方法testDoValidateException用来测试对错误的email格式是否能正确触发异常。
这个例子的关键是方法testDoValidateException(String email) 。
import junit.framework.TestCase;
public class TestEmailAddrValidator extends TestCase {
EmailAddrValidator validator = new EmailAddrValidator();
public void testDoValidate() throws ValidationException {
validator.doValidate("
[email protected]", null);
validator.doValidate("
[email protected]", null);
validator.doValidate("
[email protected]", null);
validator.doValidate("glchen.gang@163_tom.com", null);
}
public void testDoValidateException() {
testDoValidateException("@b.c");
testDoValidateException("
[email protected]");
testDoValidateException("a@b.");
testDoValidateException("@.c");
testDoValidateException("@...");
testDoValidateException(" ");
testDoValidateException(null);
}
private void testDoValidateException(String email) {
try {
validator.doValidate(email, null);
fail("末抛出异常");
} catch (ValidationException e) {
assertTrue(true);
}
}
}
-----------------------------------------
注:在这里Locale 参数并没有用到。
import java.util.Locale;
import com.hygensoft.common.configure.ConfigureObject;
public class EmailAddrValidator{
protected static final String ERROR_CODE_INVALID_EMAIL_ADDR = "INVALID_EMAIL_ADDR";
protected static final String ERROR_CODE_INVALID_INPUT = "INVALID_INPUT_OBJECT";
public Object doValidate(Object input, Locale locale) throws ValidationException {
if (!(input instanceof String)) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
String inputStr = (String) input;
int idx = inputStr.indexOf('@');
if (idx == -1 || idx == 0) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
int idx2 = inputStr.indexOf('.', idx);
if (idx2 == -1 || idx2 == idx + 1) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
if (inputStr.endsWith(".")) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
return input;
}
/* (non-Javadoc)
* @see com.hygensoft.common.configure.Configurable#initialize(com.hygensoft.common.configure.ConfigureObject)
*/
public void initialize(ConfigureObject conf) {}
}
[关键字]JUnit、测试异常
很多时候,我们要写一些单元测试来测试我们程序是否能正确触发异常。
比如下面的例子中,我们就写了一个Test Case来测试一个EmAIl验证类EmailAddrValidator,这个类有一个doValidate(email)方法可以验证email是否合法,如果不合法则会抛出ValidationException异常。因此我们写了两个方法来进行单元测试,前一个方法tEStDoValidate用来测试正常值,后一个方法teSTDoValidateException用来测试对错误的email格式是否能正确触发异常。
这个例子的关键是方法testDoValidateException(String email) 。
import junit.Framework.TestCASe;
public Class TestEmailAddrValidator extends TestCase {
EmailAddrValidator validator = new EmailAddrValidator();
public void testDoValidate() throws ValidationException {
validator.doValidate("
[email protected]", null);
validator.doValidate("
[email protected]", null);
validator.doValidate("
[email protected]", null);
validator.doValidate("glchen.gang@163_tom.com", null);
}
public void testDoValidateException() {
testDoValidateException("@b.c");
testDoValidateException("
[email protected]");
testDoValidateException("a@b.");
testDoValidateException("@.c");
testDoValidateException("@...");
testDoValidateException(" ");
testDoValidateException(null);
}
private void testDoValidateException(String email) {
try {
validator.doValidate(email, null);
fail("末抛出异常");
} catch (ValidationException e) {
assertTrue(true);
}
}
}
-----------------------------------------
注:在这里Locale 参数并没有用到。
import java.util.Locale;
import com.hygensoft.common.configure.Configureobject;
public class EmailAddrValidator{
protECted static final String ERROR_CODE_INVALID_EMAIL_ADDR = "INVALID_EMAIL_ADDR";
protected static final String ERROR_CODE_INVALID_INPUT = "INVALID_INPUT_OBJECT";
public Object doValidate(Object input, Locale locale) throws ValidationException {
if (!(input instanceof String)) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
String inputStr = (String) input;
int idx = inputStr.indexOf('@');
if (idx == -1 || idx == 0) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
int idx2 = inputStr.indexOf('.', idx);
if (idx2 == -1 || idx2 == idx + 1) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
if (inputStr.endsWith(".")) {
throw new ValidationException(ERROR_CODE_INVALID_INPUT, input);
}
return input;
}
/* (non-JavADOc)
* @see com.hygensoft.common.configure.Configurable#initialize(com.hygensoft.common.configure.ConfigureObject)
*/
public void initialize(ConfigureObject conf) {}
}
junit assert() 使用实例
package com.liyingcheng.netTest;
import com.liyingcheng.net.Sort;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.xiaoxuhui.Sum;
public class SortTest extends TestCase
{
// Sort popObj = new Sort();
public SortTest(String name)
{
super(name);
}
protected void setUp() throws Exception
{
super.setUp();
}
protected void tearDown() throws Exception
{
super.tearDown();
}
static public void assertEquals(int[] expected, int[] actual)
{
for(int i = 0; i < expected.length; i++)
{
assertEquals(null, expected[i], actual[i]);
}
}
public void testPopSort()
{
int[] expected = new int[] {1, 2, 3, 4};
assertEquals(expected, Sort.popSort(new int[] {1, 2, 4, 3}));
assertEquals(expected, Sort.popSort(new int[] {1, 2, 3, 4}));
assertEquals(expected, Sort.popSort(new int[] {1, 3, 4, 2}));
assertEquals(expected, Sort.popSort(new int[] {1, 3, 2, 4}));
assertEquals(expected, Sort.popSort(new int[] {2, 1, 4, 3}));
assertEquals(expected, Sort.popSort(new int[] {2, 4, 1, 3}));
assertEquals(expected, Sort.popSort(new int[] {3, 2, 4, 1}));
assertEquals(new int[] {1, 2}, Sort.popSort(new int[] {2, 1}));
// assertEquals(new int[]{1,3,2,4},popObj.popSort(new int[]{1,2,4,3}));
// assertEquals(new int[]{1,2,3,4},popObj.popSort(new int[]{1,2,4,3}));
}
public void testCreateArray()
{
assertEquals(4, Sort.createArray(4).length);
}
public void testGetSum()
{
assertEquals(5050, Sum.getSum(1, 100));
}
public void testFalse()
{
assertFalse(false);
assertTrue(true);
}
public void testIsNull()
{
String str1 = "";
int[] arr1 = {};
String str2 = null;
int[] arr2 = null;
assertNotNull(str1);
assertNotNull(arr1);
assertNull(str2);
assertNull(arr2);
// assertNull(str);
}
public void testNull()
{
}
public void testNotSame()
{
String str1 = "123";
String str2 = "123";
String str3 = new String(str1);
String str4 = new String("123");
int one = 1;
int first = 1;
assertSame(one, first);
assertSame(str1, str2);
assertNotSame(str3, str4);
//fail("hahahahahahahah");
/*
* assertNotSame(one,first); assertNotSame(str1,str2);
*/
}
public static Test suite()
{
TestSuite suite = new TestSuite("Test sort!");
suite.addTestSuite(SortTest.class);
return suite;
}
}
package junit.sineat.templet; import java.util.Hashtable; import junit.framework.Assert; import junit.framework.TestCase; import junit.framework.TestSuite; public class JunitB extends TestCase...{ /** *//**定义你需要测试的类及用到的变量*****************************/ public Hashtable hasha=null;// public Hashtable hashb=null; /** *//*******************************************************/ public JunitB(String name)...{ super(name);//创建子类 } /** *//**用setUp进行初始化操作*/ protected void setUp() throws Exception ...{ super.setUp(); hasha =new Hashtable();//这里 } /** *//**用tearDown来销毁所占用的资源*/ protected void tearDown() throws Exception ...{ super.tearDown(); //System.gc(); } /** *//**写一个测试方法断言期望的结果**/ public void testBodyStatus() ...{ //hasha =new Hashtable();//有此句后也可去掉setUp() tearDown() assertNotNull(hasha); //hasha.put("0","let's try again");//test1.error版 assertTrue(hasha.isEmpty());//期望为空 } /** *//**再写一个测试方法断言期望的结果**/ public void testBodySame() ...{ //hashb=(Hashtable)hasha.clone(); //test2.error版 hashb=hasha; //test2.OK 版 Assert.assertSame(hasha,hashb); } /** *//**suite()方法,使用反射动态的创建一个包含所有的testXxxx方法的测试套件**/ public static TestSuite suite() ...{ return new TestSuite(JunitB.class); } /** *//****写一个main()运行测试*****************/ public static void main(String args[]) ...{ junit.textui.TestRunner.run(suite());//以文本运行器的方式方便的 //junit.swingui.TestRunner.run(JunitB.class); } }
package jp.planstaff.dao;
import java.util.Date;
import jp.planstaff.model.User;
import junit.framework.TestCase;
public class UserDAOTest extends TestCase {
public void testGetInstance() {
UserDAO dao = UserDAO.getInstance();
assertNotNull(dao);
}
public void testSave() throws Exception {
User user = new User("hira", "kata", 1, new Date(), "
[email protected]",
"1234-123", "1234-123", "aaaa", "bbbb", "asfaewr", "awerasr",
"prprrprr", new Date());
UserDAO.getInstance().save(user);
assertNotNull(user.getId());
}
public void testUpdate() throws Exception {
User user = new User("hira", "kata", 1, new Date(), "
[email protected]",
"1234-123", "1234-123", "aaaa", "bbbb", "asfaewr", "awerasr",
"prprrprr", new Date());
UserDAO.getInstance().save(user);
User user1 = UserDAO.getInstance().findById(user.getId());
user1.setHira("hirahira");
user1.setKata("katakata");
UserDAO.getInstance().update(user1);
assertEquals("hirahira", user1.getHira());
assertEquals("katakata", user1.getKata());
assertEquals(user.getId(), user1.getId());
}
public void testFindAll() throws Exception {
int size1 = UserDAO.getInstance().findAll().size();
User user = new User("hira", "kata", 1, new Date(), "
[email protected]",
"1234-123", "1234-123", "aaaa", "bbbb", "asfaewr", "awerasr",
"prprrprr", new Date());
UserDAO.getInstance().save(user);
int size2 = UserDAO.getInstance().findAll().size();
assertNotSame(new Integer(size1), new Integer(size2));
}
public void testDelete() throws Exception {
User user = new User("hira", "kata", 1, new Date(), "
[email protected]",
"1234-123", "1234-123", "aaaa", "bbbb", "asfaewr", "awerasr",
"prprrprr", new Date());
UserDAO.getInstance().save(user);
User user1 = new User();
user1.setId(user.getId());
UserDAO.getInstance().delete(user1);
}
}
例2是一个用JUnit测试例1的单例模式的案例:
例2.一个单例模式的案例
import org.apache.log4j.Logger;
import junit.framework.Assert;
import junit.framework.TestCase;
public class SingletonTest extends TestCase {
private ClassicSingleton sone = null, stwo = null;
private static Logger logger = Logger.getRootLogger();
public SingletonTest(String name) {
super(name);
}
public void setUp() {
logger.info("getting singleton...");
sone = ClassicSingleton.getInstance();
logger.info("...got singleton: " + sone);
logger.info("getting singleton...");
stwo = ClassicSingleton.getInstance();
logger.info("...got singleton: " + stwo);
}
public void testUnique() {
logger.info("checking singletons for equality");
Assert.assertEquals(true, sone == stwo);
}
}
例2两次调用ClassicSingleton.getInstance(),并且把返回的引用存储在成员变量中。方法testUnique()会检查这些引用看它们是否相同。例3是这个测试案例的输出:
例3.是这个测试案例的输出
Buildfile: build.xml
init:
[echo] Build 20030414 (14-04-2003 03:08)
compile:
run-test-text:
[java] .INFO main:
getting singleton...
[java] INFO main:
created singleton: Singleton@e86f41
[java] INFO main: ...got singleton: Singleton@e86f41
[java] INFO main:
getting singleton...
[java] INFO main: ...got singleton: Singleton@e86f41
[java] INFO main: checking singletons for equality
[java] Time: 0.032
[java] OK (1 test)
正如前面的清单所示,例2的简单测试顺利通过----通过ClassicSingleton.getInstance()获得的两个单例类的引用确实相同;然而,你要知道这些引用是在单线程中得到的。下面的部分着重于用多线程测试单例类。
多线程因素的考虑
在例1中的ClassicSingleton.getInstance()方法由于下面的代码而不是线程安全的:
1: if(instance == null) {
2: instance = new Singleton();
3: }
如果一个线程在第二行的赋值语句发生之前切换,那么成员变量instance仍然是null,然后另一个线程可能接下来进入到if块中。在这种情况下,两个不同的单例类实例就被创建。不幸的是这种假定很少发生,这样这种假定也很难在测试期间出现(译注:在这可能是作者对很少出现这种情况而导致无法测试从而使人们放松警惕而感到叹惜)。为了演示这个线程轮换,我得重新实现例1中的那个类。例4就是修订后的单例类:
例4.人为安排的方式
import org.apache.log4j.Logger;
public class Singleton {
private static Singleton singleton = null;
private static Logger logger = Logger.getRootLogger();
private static boolean firstThread = true;
protected Singleton() {
// Exists only to defeat instantiation.
}
public static Singleton getInstance() {
if(singleton == null) {
simulateRandomActivity();
singleton = new Singleton();
}
logger.info("created singleton: " + singleton);
return singleton;
}
private static void simulateRandomActivity() {
try {
if(firstThread) {
firstThread = false;
logger.info("sleeping...");
// This nap should give the second thread enough time
// to get by the first thread.
Thread.currentThread().sleep(50);
}
}
catch(InterruptedException ex) {
logger.warn("Sleep interrupted");
}
}
}
除了在这个清单中的单例类强制使用了一个多线程错误处理,例4类似于例1中的单例类。在getInstance()方法第一次被调用时,调用这个方法的线程会休眠50毫秒以便另外的线程也有时间调用getInstance()并创建一个新的单例类实例。当休眠的线程觉醒时,它也会创建一个新的单例类实例,这样我们就有两个单例类实例。尽管例4是人为如此的,但它却模拟了第一个线程调用了getInstance()并在没有完成时被切换的真实情形。
例5测试了例4的单例类:
例5.失败的测试
import org.apache.log4j.Logger;
import junit.framework.Assert;
import junit.framework.TestCase;
public class SingletonTest extends TestCase {
private static Logger logger = Logger.getRootLogger();
private static Singleton singleton = null;
public SingletonTest(String name) {
super(name);
}
public void setUp() {
singleton = null;
}
public void testUnique() throws InterruptedException {
// Both threads call Singleton.getInstance().
Thread threadOne = new Thread(new SingletonTestRunnable()),
threadTwo = new Thread(new SingletonTestRunnable());
threadOne.start();
threadTwo.start();
threadOne.join();
threadTwo.join();
}
private static class SingletonTestRunnable implements Runnable {
public void run() {
// Get a reference to the singleton.
Singleton s = Singleton.getInstance();
// Protect singleton member variable from
// multithreaded access.
synchronized(SingletonTest.class) {
if(singleton == null) // If local reference is null...
singleton = s; // ...set it to the singleton
}
// Local reference must be equal to the one and
// only instance of Singleton; otherwise, we have two
// Singleton instances.
Assert.assertEquals(true, s == singleton);
}
}
}
例 5的测试案例创建两个线程,然后各自启动,等待完成。这个案例保持了一个对单例类的静态引用,每个线程都会调用 Singleton.getInstance()。如果这个静态成员变量没有被设置,那么第一个线程就会将它设为通过调用getInstance()而得到的引用,然后这个静态变量会与一个局部变量比较是否相等。
在这个测试案例运行时会发生一系列的事情:第一个线程调用 getInstance(),进入if块,然后休眠;接着,第二个线程也调用getInstance()并且创建了一个单例类的实例。第二个线程会设置这个静态成员变量为它所创建的引用。第二个线程检查这个静态成员变量与一个局部备份的相等性。然后测试通过。当第一个线程觉醒时,它也会创建一个单例类的实例,并且它不会设置那个静态成员变量(因为第二个线程已经设置过了),所以那个静态变量与那个局部变量脱离同步,相等性测试即告失败。例6列出了例5的输出:
例6.例5的输出
Buildfile: build.xml
init:
[echo] Build 20030414 (14-04-2003 03:06)
compile:
run-test-text:
INFO Thread-1: sleeping...
INFO Thread-2: created singleton: Singleton@7e5cbd
INFO Thread-1: created singleton: Singleton@704ebb
junit.framework.AssertionFailedError: expected: but was:
at junit.framework.Assert.fail(Assert.java:47)
at junit.framework.Assert.failNotEquals(Assert.java:282)
at junit.framework.Assert.assertEquals(Assert.java:64)
at junit.framework.Assert.assertEquals(Assert.java:149)
at junit.framework.Assert.assertEquals(Assert.java:155)
at SingletonTest$SingletonTestRunnable.run(Unknown Source)
at java.lang.Thread.run(Thread.java:554)
[java] .
[java] Time: 0.577
[java] OK (1 test)
到现在为止我们已经知道例4不是线程安全的,那就让我们看看如何修正它。
同步
要使例4的单例类为线程安全的很容易----只要像下面一个同步化getInstance()方法:
public synchronized static Singleton getInstance() {
if(singleton == null) {
simulateRandomActivity();
singleton = new Singleton();
}
logger.info("created singleton: " + singleton);
return singleton;
}
在同步化getInstance()方法后,我们就可以得到例5的测试案例返回的下面的结果:
Buildfile: build.xml
init:
[echo] Build 20030414 (14-04-2003 03:15)
compile:
[javac] Compiling 2 source files
run-test-text:
INFO Thread-1: sleeping...
INFO Thread-1: created singleton: Singleton@ef577d
INFO Thread-2: created singleton: Singleton@ef577d
[java] .
[java] Time: 0.513
[java] OK (1 test)
这此,这个测试案例工作正常,并且多线程的烦恼也被解决;然而,机敏的读者可能会认识到getInstance()方法只需要在第一次被调用时同步。因为同步的性能开销很昂贵(同步方法比非同步方法能降低到100次左右),或许我们可以引入一种性能改进方法,它只同步单例类的getInstance()方法中的赋值语句。
一种性能改进的方法
寻找一种性能改进方法时,你可能会选择像下面这样重写getInstance()方法:
public static Singleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
这个代码片段只同步了关键的代码,而不是同步整个方法。然而这段代码却不是线程安全的。考虑一下下面的假定:线程1进入同步块,并且在它给 singleton成员变量赋值之前线程1被切换。接着另一个线程进入if块。第二个线程将等待直到第一个线程完成,并且仍然会得到两个不同的单例类实例。有修复这个问题的方法吗?请读下去。
双重加锁检查
初看上去,双重加锁检查似乎是一种使懒汉式实例化为线程安全的技术。下面的代码片段展示了这种技术:
public static Singleton getInstance() {
if(singleton == null) {
synchronized(Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
如果两个线程同时访问getInstance()方法会发生什么?想像一下线程1进行同步块马上又被切换。接着,第二个线程进入if 块。当线程1退出同步块时,线程2会重新检查看是否singleton实例仍然为null。因为线程1设置了singleton成员变量,所以线程2的第二次检查会失败,第二个单例类实例也就不会被创建。似乎就是如此。
不幸的是,双重加锁检查不会保证正常工作,因为编译器会在Singleton的构造方法被调用之前随意给singleton赋一个值。如果在singleton引用被赋值之后而被初始化之前线程1被切换,线程2就会被返回一个对未初始化的单例类实例的引用。
一个改进的线程安全的单例模式实现
例7列出了一个简单、快速而又是线程安全的单例模式实现:
例7.一个简单的单例类
public class Singleton {
public final static Singleton INSTANCE = new Singleton();
private Singleton() {
// Exists only to defeat instantiation.
}
}
这段代码是线程安全的是因为静态成员变量一定会在类被第一次访问时被创建。你得到了一个自动使用了懒汉式实例化的线程安全的实现;你应该这样使用它:
Singleton singleton = Singleton.INSTANCE;
singleton.dothis();
singleton.dothat();
...
当然万事并不完美,前面的Singleton只是一个折衷的方案;如果你使用那个实现,你就无法改变它以便后来你可能想要允许多个单例类的实例。用一种更折哀的单例模式实现(通过一个getInstance()方法获得实例)你可以改变这个方法以便返回一个唯一的实例或者是数百个实例中的一个.你不能用一个公开且是静态的(public static)成员变量这样做.
你可以安全的使用例7的单例模式实现或者是例1的带一个同步的getInstance()方法的实现.然而,我们必须要研究另一个问题:你必须在编译期指定这个单例类,这样就不是很灵活.一个单例类的注册表会让我们在运行期指定一个单例类.