15 Jul 2014

Java设计模式:单例

注:本文为译文,原文出处java-design-patterns-in-stories

单例是最广泛应用的Java设计模式之一. 它通过防止外部对象实例化和修改来控制对象的数量. 这个概念适用于那些当只有一个对象却可以更高效运作的系统,或者那些限制一个特定对象实例数量的系统,例如:

  1. 私有构造器 - 没有其他类可以实例化新对象
  2. 私有引用- 不存在外部修改
  3. 公有静态方法 -唯一可以获得对象的途径

单例的故事

这是一个简单的用例. 一个国家只能有一个总统. 因此当需要总统的时候, 只应该返回唯一的总统, 而不是创建一个新的. getPresident()方法将会保证只有一个总统会被创建.

类图和代码

singleton

预加载模式:

public class AmericaPresident {
    private static final AmericaPresident thePresident = new AmericaPresident();

    private AmericaPresident() {}

    public static AmericaPresident getPresident() {
        return thePresident;
    }
}

thePresident声明为final, 因此它将总是持有同一个对象引用.

懒加载模式1(无法保证线程安全):

public class AmericaPresident {
    private static AmericaPresident thePresident;

    private AmericaPresident() {}

    public static AmericaPresident getPresident() {
        if (thePresident == null) {
            thePresident = new AmericaPresident();
        }
        return thePresident;
    }
}

懒加载模式2(线程安全):

public class AmericaPresident {
    private static AmericaPresident thePresident;
    private AmericaPresident (){}
    public static synchronized AmericaPresident getPresident() {
    if (thePresident == null) {
        thePresident = new AmericaPresident();
    }
    return thePresident;
    }
}

虽然上述方式保证了线程的安全, 但是效率很低,因此有人提出了一种新的方式:双重校验锁.

懒加载模式3(双重校验锁1):

public class AmericaPresident {
    private volatile static AmericaPresident thePresident;
    private AmericaPresident (){}
    public static AmericaPresident getPresident() {
    if (thePresident == null) {
        synchronized (AmericaPresident.class) {
        if (thePresident == null) {
            thePresident = new AmericaPresident();
        }
        }
    }
    return thePresident;
    }
}

开发人员试图建立一种保证单例,但是又不会在线程同步上损耗太多事件成本的方法.但是此方法也存在问题: 假设AmericaPresident还没有被实例化. 接着, 线程A通过helper==null的判断, 进入同步块并开始执行thePresident = new AmericaPresident(). 与此同时,在AmericaPresident构造函数还没有完成执行完成时候, 线程B判断thePresident不为null, 并且使用thePresident的实例变量, 则会接收到不正确的值. 参见CWE-609: Double-Checked Locking

懒加载模式4(双重校验锁2):

public class AmericaPresident {
   private static AmericaPresident thePresident = null;

   private AmericaPresident() { }

   public static AmericaPresident getPresident() {
      if(thePresident == null) {
         synchronized(AmericaPresident.class) {
            AmericaPresident temp = thePresident;
            if(temp == null) {
               temp = new AmericaPresident();
               thePresident = temp
            }
         }
      }

      return thePresident;
   }
}

public class AmericaPresident {
   private static volatile AmericaPresident thePresident = null;

   private AmericaPresident() { }

   public static AmericaPresident getPresident() {
      if(thePresident == null) {
         synchronized(AmericaPresident.class) {
            if(thePresident == null) {
               thePresident = new AmericaPresident();
            }
         }
      }

      return thePresident;
   }
}

Java Stand Library中单例模式的应用

java.lang.Runtime#getRuntime() 是一个JSL中被广泛使用的方法. getRunTime()返回关联当前Java应用程序的运行时对象.

class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}

    //...
}

这是一个使用getRunTime()的简单的例子. 它在Windows上读取一个网页.

Process p = Runtime.getRuntime().exec(
        "C:/windows/system32/ping.exe programcreek.com");
//get process input stream and put it to buffered reader
BufferedReader input = new BufferedReader(new InputStreamReader(
        p.getInputStream()));

String line;
while ((line = input.readLine()) != null) {
    System.out.println(line);
}

input.close();

输出:

Pinging programcreek.com [198.71.49.96] with 32 bytes of data:
Reply from 198.71.49.96: bytes=32 time=53ms TTL=47
Reply from 198.71.49.96: bytes=32 time=53ms TTL=47
Reply from 198.71.49.96: bytes=32 time=52ms TTL=47
Reply from 198.71.49.96: bytes=32 time=53ms TTL=47

Ping statistics for 198.71.49.96:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
    Minimum = 52ms, Maximum = 53ms, Average = 52ms

另一个单例实现

私有构造器并不会防止通过反射进行实例化, Joshua Bloch (Effective Java)提出了一个更加优雅的单例实现. 如果你对于Enum不够熟悉, 下面则是一个Oracle中很好的例子.

public enum AmericaPresident{
    INSTANCE;

    public static void doSomething(){
        //do something
    }
}

Tags:
0 comments