摘要:
Singleton模式(单例模式)是非常常用的一个设计模式,用于控制对象的数量,防止被外界初始化和修改等,在java中的作用是保证一个Class只有一个实例存在。经常在一些操作中,比如IO处理、数据库操作、无状态的工具服务类等我们只需要有一个实例存在即可完成我们的工作,而创建多个实例可能是一项很消耗系统资源的工作。使用Singleton的好处就在于可以节省内存,加速资源的获取速度,因为它限制了实例的个数,有利于Java垃圾回收等工作;说Singleton模式很简单是因为她只需要一个类就可以完成,但是如果涉及考虑DCL双锁检测(double checked locking)、多个类加载器(ClassLoader)协同时、跨JVM(集群、远程EJB等)时、单例对象被销毁后重建等讨论的话,这个模式又是非常复杂的,这里只考虑简单的单例模式实现。
设计模型:
场景:
在美国只能有一个总统,无论何时何地当需要使用总统对象时,只能放回同一个对象。
实现Singleton的常用方式:
最常用的饿汉式:
public class AmericaPresident {
private AmericaPresident() {}
private static final AmericaPresident THE_PRESIDENT = new AmericaPresident();
public static AmericaPresident getPresident() {
return THE_PRESIDENT;
}
}
这种模式在ClassLoader加载类后第一时间被创建,但在某些情况下将无法使用,比如创建过程需要一些配置参数等依赖其它一些工作,另外这种创建方式会增加类加载时的负担,而且可能创建出的实例很长时间都不使用,于是有了下面的实现方式,
懒汉式:
public class AmericaPresident {
private AmericaPresident() {}
private static AmericaPresident thePresident;
public static AmericaPresident getPresident() {
if (thePresident == null) thePresident = new AmericaPresident();
return thePresident;
}
}
Java标准类库中的单例:Runtime 类
大家经常使用的 java.lang.Runtime#getRuntime()
总是返回同一个对象(关联自己java应用的运行时对象),查看源码:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
一个使用
getRunTime()
的例子:
public static void main(String[] args) throws IOException {
// Process p = Runtime.getRuntime().exec("ping www.baidu.com");
Process p = Runtime.getRuntime().exec("ifconfig");
//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();
}
上文中的懒汉式在多线程的环境下,可能会多次执行:new AmericaPresident();为了避免这个问题,可做如下修订:
推荐的懒汉式:
public class AmericaPresident {
private AmericaPresident() {}
private static class SingletonHolder {
private static final AmericaPresident INSTANCE = new AmericaPresident();
}
public static AmericaPresident getPresident() {
return SingletonHolder.INSTANCE;
}
}
这种方式使用了静态内部类持有单例对象,使用JVM本身机制保证了线程安全问题。由于SingletonHolder是私有的,只有getInstance()可以访问此类,所以只有在第一次访问这个方法的时候
SingletonHolder才会被加载进而创建Singleton,因此它是懒汉式的,而且是线程安全的。
另种实现方式:
上面的实现方式中,不能阻止使用反射技术对其进行实例化,使用枚举的实现方式可以解决这个问题:
public enum AmericaPresident {
INSTANCE;
}
此方式十分简洁,利用java的枚举机制,保证了单例,且如有需要还可以保证同时存在多个对象(添加多个枚举实例即可)。
========== 关于双检锁 ==========
下面是使用双检锁对单例模式的实现代码:
思路很简单,就是我们只需要同步初始化的那部分代码从而使代码既正确又很有效率,class SingletonClass { private SingletonClass() { } private static SingletonClass instance; public static SingletonClass getInstance() { if (instance == null) { synchronized (SingletonClass.class) { if (instance == null) { instance = new SingletonClass(); } } } return instance; } // other methods and members... }
这就是所谓的“双检锁”机制(顾名思义)。
很可惜,这样的写法在很多平台和优化编译器上是错误的。
原因在于:
instance = new SingletonClass();
这行代码在不同编译器上的行为是无法预知的。一个优化编译器
可以合法地如下实现
:
@1.instance = 给新的实体分配内存
@2.调用SingletonClass的构造函数来初始化它的成员变量
现在想象一下,有线程A和B在调用 getInstance(),
线程A先进入,在执行完 步骤@1 后被踢出了cpu(还没有执行步骤@2),此时线程B进入,B看到的是instance 已经不是null了(A线程的在步骤@1时已经分配内存,分配了内存判断就不是null了),这个时候使用instance就会产生错误,
因为在这一时刻,instance只是分配了内存,但其成员变量还都是缺省值,A还没 有来得及执行步骤@2来完成instance的初始化。