单例模式应该是我们接触的众多设计模式中的第一个,但是对于单例模式的一些细节地方对于初学者来说并不是很清楚,所以本文就来整理下单例模式。
单例模式
单例模式的核心是保证一个类只有一个实例,并且提供一个访问实例的全局访问点。
单例的使用场景
Spring中bean对象的模式实现方式
servlet中每个servlet的实例
spring mvc和struts1框架中,控制器对象是单例模式
应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加
操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统
项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源
…
单例模式优点
由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理
单例的实现方式
饿汉式
也就是类加载的时候立即实例化对象,实现的步骤是先私有化构造方法,对外提供唯一的静态入口方法,实现如下
/** * 单例模式:饿汉式 * @author 波波烤鸭 * */ public class SingletonInstance1 { // 声明此类型的变量,并实例化,当该类被加载的时候就完成了实例化并保存在了内存中 private static SingletonInstance1 instance = new SingletonInstance1(); // 私有化所有的构造方法,防止直接通过new关键字实例化 private SingletonInstance1(){} // 对外提供一个获取实例的静态方法 public static SingletonInstance1 getInstance(){ return instance; } }
饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字
问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!
懒汉式
/** * 单例模式:懒汉式 * @author 波波烤鸭 * */ public class SingletonInstance2 { // 声明此类型的变量,但没有实例化 private static SingletonInstance2 instance = null; // 私有化所有的构造方法,防止直接通过new关键字实例化 private SingletonInstance2(){} // 对外提供一个获取实例的静态方法,为了数据安全添加synchronized关键字 public static synchronized SingletonInstance2 getInstance(){ if(instance == null){ // 当instance不为空的时候才实例化 instance = new SingletonInstance2(); } return instance; } }
此种方式在类加载后如果我们一直没有调用getInstance方法,那么就不会实例化对象。实现了延迟加载,但是因为在方法上添加了synchronized关键字,每次调用getInstance方法都会同步,所以对性能的影响比较大。
双重检测锁式
/** * 单例模式:懒汉式 * 双重检测机制 * @author 波波烤鸭 * */ public class SingletonInstance3 { // 声明此类型的变量,但没有实例化 private static SingletonInstance3 instance = null; // 私有化所有的构造方法,防止直接通过new关键字实例化 private SingletonInstance3(){} // 对外提供一个获取实例的静态方法, public static SingletonInstance3 getInstance(){ if(instance == null){ SingletonInstance3 s3 = null; synchronized(SingletonInstance3.class){ s3 = instance; if(s3 == null){ synchronized(SingletonInstance3.class){ if(s3 == null){ s3 = new SingletonInstance3(); } } } instance = s3; } } return instance; } }
这个模式将同步内容下方到if内部,提高了执行的效率不必每次获取对象时都进行同步,只有第一次才同步创建了以后就没必要了。
问题:由于编译器优化原因和JVM底层内部模型原因,偶尔会出问题。不建议使用。
静态内部类式
/** * 静态内部类实现方式 * @author 波波烤鸭 * */ public class SingletonInstance4 { // 静态内部类 public static class SingletonClassInstance{ // 声明外部类型的静态常量 public static final SingletonInstance4 instance = new SingletonInstance4(); } // 私有化构造方法 private SingletonInstance4(){} // 对外提供的唯一获取实例的方法 public static SingletonInstance4 getInstance(){ return SingletonClassInstance.instance; } }
注意点:
外部类没有static属性,则不会像饿汉式那样立即加载对象。
只有真正调用getInstance(),才会加载静态内部类。加载类时是线程 安全的instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性.
兼备了并发高效调用和延迟加载的优势!– 兼备了并发高效调用和延迟加载的优势!
枚举单例
/** * 单例模式:枚举方式实现 * @author dengp * */ public enum SingletonInstance5 { // 定义一个枚举元素,则这个元素就代表了SingletonInstance5的实例 INSTANCE; public void singletonOperation(){ // 功能处理 } }
测试代码
public static void main(String[] args) { SingletonInstance5 s1 = SingletonInstance5.INSTANCE; SingletonInstance5 s2 = SingletonInstance5.INSTANCE; System.out.println(s1 == s2); // 输出的是 true }
优点:
实现简单
枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
缺点:
无延迟加载
单例模式漏洞
通过反射的方式我们依然可用获取多个实例(除了枚举的方式)
public static void main(String[] args) throws Exception, IllegalAccessException { SingletonInstance1 s1 = SingletonInstance1.getInstance(); // 反射方式获取实例 Class c1 = SingletonInstance1.class; Constructor constructor = c1.getDeclaredConstructor(null); constructor.setAccessible(true); SingletonInstance1 s2 = (SingletonInstance1)constructor.newInstance(null); System.out.println(s1); System.out.println(s2); }
输出结果:
com.dpb.single.SingletonInstance1@15db9742 com.dpb.single.SingletonInstance1@6d06d69c
产生了两个对象,和单例的设计初衷违背了。
解决的方式是在无参构造方法中手动抛出异常控制
// 私有化所有的构造方法,防止直接通过new关键字实例化 private SingletonInstance2(){ if(instance != null){ // 只能有一个实例存在,如果再次调用该构造方法就抛出异常,防止反射方式实例化 throw new RuntimeException("单例模式只能创建一个对象"); } }
通过反序列化的方式也可以破解上面几种方式(除了枚举的方式)
public static void main(String[] args) throws Exception, IllegalAccessException { SingletonInstance2 s1 = SingletonInstance2.getInstance(); // 将实例对象序列化到文件中 ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("c:/tools/a.txt")); oos.writeObject(s1); oos.flush(); oos.close(); // 将实例从文件中反序列化出来 ObjectInputStream ois = new ObjectInputStream( new FileInputStream("c:/tools/a.txt")); SingletonInstance2 s2 = (SingletonInstance2) ois.readObject(); ois.close(); System.out.println(s1); System.out.println(s2); }
输出结果:
com.dpb.single.SingletonInstance2@5c647e05 com.dpb.single.SingletonInstance2@4c873330
是两个不同的对象,同样破坏了单例模式,这种情况怎么解决呢
我们只需要在单例类中重写readResolve方法并在该方法中返回单例对象即可,如下:
package com.dpb.single; import java.io.ObjectStreamException; import java.io.Serializable; /** * 单例模式:懒汉式 * @author 波波烤鸭 * */ public class SingletonInstance2 implements Serializable{ // 声明此类型的变量,但没有实例化 private static SingletonInstance2 instance = null; // 私有化所有的构造方法,防止直接通过new关键字实例化 private SingletonInstance2(){ if(instance != null){ // 只能有一个实例存在,如果再次调用该构造方法就抛出异常,防止反射方式实例化 throw new RuntimeException("单例模式只能创建一个对象"); } } // 对外提供一个获取实例的静态方法,为了数据安全添加synchronized关键字 public static synchronized SingletonInstance2 getInstance(){ if(instance == null){ // 当instance不为空的时候才实例化 instance = new SingletonInstance2(); } return instance; } // 重写该方法,防止序列化和反序列化获取实例 private Object readResolve() throws ObjectStreamException{ return instance; } }
说明:readResolve方法是基于回调的,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象,而不需要在创建新的对象!
常见的五种单例模式在多线程环境下的效率测试
大家只要关注相对值即可。在不同的环境下不同的程序测得值完全不一样
总结
这几种设计模式如何选择
1、单例对象占用资源少,不需要延时加载:
枚举式 好于 饿汉式
2、单例对象占用资源大,需要延时加载:
静态内部类式 好于 懒汉式