单例模式定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式主要的关键点:
1.构造函数私有:private;
2.通过一个静态方法或者枚举返回单例类对象;
3.确保单例类的对象有且只有一个,尤其是在多线程环境下;
4.确保单例类对象在反序列化时不会重新构建对象。
1.饿汉式,也是最简单的一种书写方法
public class Singleton{
//饿坏了,上来先new一个实例,边吃边写。
private static final Singleton mSingleton = new Singleton();
private Singleton(){
//构造函数私有化,防止被其他人new出来。
}
//公有的静态函数给外界一个机会得到这个单例的实例(很好得,一得就能得到)
public static Singleton getSingleton(){
return mSingleton;
}
}
这种情况下,其他类若想得到这个类的实例,就只能通过:Singleton.getSingleton()函数来获取,由于这个对象是静态的,并且在声明的时候已经初始化,因此保证了Singleton对象的唯一性。
2.懒汉式
public class Singleton{
//我有点懒,不想管,等会儿吧...
private static Singleton instance;
private Singleton(){
//构造函数私有化
}
//该方法添加了一把同步锁:synchronized,以此来保证在多线程的情况下单例对象的唯一性
public static synchronized Singleton getInstance(){
if(instance==null){
instance= new Singleton ();
}
return instance;
}
}
懒汉式的优点在于该类在使用时才会被实例化,节约了那么一丢丢的资源;
缺点是第一次加载时需要及时进行实例化,反应稍慢
最大的问题是:每次都要进行同步,但是这个同步锁只在第一次实例化时有意义,之后的同步只会造成不必要的内存开销。
所以!这种模式一般不建议使用!(可能有人会发牢骚说不建议使用你还说,这不是误人子弟吗。我之前也有过这种想法,可后来我想明白了:学习,不光要知道什么是对的,更要知道什么是不太合适的,甚至是错的。做人做事都一样,我们不光要知道学雷锋做好事是对的,也要知道参与黄赌毒是违法的。)
3.Double Check Lock (DCL)双重检查锁定式
public class Singleton{
//这里添加了volatile关键字,先不着急,后面再说。
private volatile static Singleton instance = null;
private Singleton(){}
//两次检查instance是否为null,一次锁定
public static Singleton getInstance() {
if(instance == null) { //第一重检查避免了不必要的同步
synchronized (Singleton.class) {
if(instance == null) { //第二重检查是为了在instance为null的情况下创建实例
instance = new Singleton();
}
}
}
return instance;
}
}
当程序执行到instance = new Singleton();这句代码最终会被编译成多条汇编指令,大致做了以下三件事:
1.给Singleton的实例分配内存;
2.调用Singleton()的构造函数,初始化成员字段;
3.将instance对象指向分配的内存空间(此时instance就不为null了)。
DCL失效问题:由于java编译器允许处理器乱序执行,以及JDK1.5之前JMM(java内存模型:Java Memory Model)中Cache、寄存器到主内存回写顺序的规定,上面的三件事中第二、三件事的顺序无法保证,有可能是1-2-3,也有可能是1-3-2,如果是后者,在3执行完毕,2还未执行时被切换到别的线程上,此时instance已经非空,所以别的线程可以直接取走instance实例,再使用时就会出错。而且这种难以跟踪不好复现的问题很可能会隐藏很久,保不齐在关键时刻(给大领导演示的时候)掉链子。
解决方法:好在JDK1.5之后,官方调整了JVM,给出一个volatile关键字,volatile通过“内存屏障”达到的作用有两个:1.是禁止指令重排;2.是将实例提升到主内存(也可以叫共享内存),让所有线程都能访问到实例的最新状态。
DCL的优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。
缺点:第一次加载时反应稍慢。
4.静态内部类单例模式
public class Singleton{
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
//静态内部类
private static class SingletonHolder{
private static final Singleton instance = new Singleton();
}
}
当第一次加载Singleton类时并不会初始化,只有在第一次调用Singleton的getInstance()方法时才会初始化。因此,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例模式实现方式。
5.枚举单例
public enum Singleton{
INSTANCE;
public void doSomething(){
System.out.println("do sth");
}
}
默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例,而且使用枚举即使反序列化也不会重新生成新的实例
上述的几种单例模式在反序列的情况下仍然可以重新创建对象,先序列化一个单例的实例到磁盘,然后再读回来获取实例,即便构造函数私有,反序列化时仍然可以通过特殊途径创建一个新的实例。反序列化操作提供了一个特殊的钩子函数,类中有个私有的、被实例化的方法readResolve(),这个方法可以让开发人员控制对象的反序列化。例如上述几个示例中如果要避免单例对象在反序列化的时候重新生成对象,就要加入如下方法:
private Object readResolve() throw ObjectStreamException{
return instance;
}
而枚举不存在反序列化重新生成实例问题。
6.使用容器实现单例模式
public class SingletonManager{
private static Map objMap = new HashMap();
private SingletonManager(){}
public static void registerService(String key,Object instance){
if(!objMap.containsKey(key)){
objMap.put(key,instance);
}
}
public static Object getService(String key){
return objMap.get(key);
}
}
在程序的初始,将多种单例类型注入到一个统一管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
总结:不管以哪一种形式实现单例模式,他们的核心原理都是将构造函数私有化,并且通过静态方法获取一个唯一的实例,在这个获取过程中必须保证线程安全、防止反序列化导致重新生成实例对象等问题。选择哪种实现方式取决于项目本身,如是否是复杂的并发环境、JDK版本是否过低、单例对象的资源消耗等。