一、概念
创建型模式
单例模式解决某个类频繁创建与销毁。该模式保证其创建的对象在JVM中只有一个实例对象存在,并且提供一个访问该实例的全局访问点。前提是:必须保证私有化构造函数且只能有一个实例对象存在。
单例模式实现过程:
1)将该类的构造函数私有化(目的是禁止其他程序创建该类的对象, 避免反射创建实例, 可以在构造函数中增加判定是否已存在一个实例);
2)在本类中自定义一个对象(已经禁止其他程序创建该类的对象,则必须自己创建一个供程序使用,否则该类无法使用,更不是单例);
3)提供一个可访问类自定义对象的类成员方法(对外提供该对象的访问方式,由于该类不可再次创建对象,则只能通过类调用,所以创建static方法)。
优点:
- 减少new关键字的使用,降低系统内存的使用频率,同时减轻GC工作;
- 避免了资源的多重使用。
缺点:
- 不可继承,没有接口。
二、分类
1、饿汉式
- 优点:线程安全,没有加锁同步,执行效率高, 不延迟加载。
- 缺点:当类加载时就初始化,没有懒加载,浪费内存,通过classloader机制避免了多线程的同步问题
1 /** 2 * 恶汉单例模式,当类加载时进行初始化,没有懒加载,浪费内存 3 */ 4 public class HungrySingleton { 5 private static HungrySingleton instance = new HungrySingleton(); 6 /** 7 * 构造函数必须私有化 8 */ 9 private HungrySingleton(){ 10 11 } 12 13 /** 14 *提供一个静态方法获取实例 15 * @return 16 */ 17 public static HungrySingleton getInstance(){ 18 return instance; 19 } 20 }
使用反射的破解与防御:
1 public class Singleton { 2 3 // 类初始化时立即创建对象 4 private static final Singleton instance = new Singleton(); 5 6 // 私有化构造器 7 private Singleton() { 8 // 防御:再次创建时抛出异常 9 if (instance != null) { 10 throw new RuntimeException(); 11 } 12 } 13 14 public static Singleton getInstance() { 15 return instance; 16 } 17 18 }
调用示例:
import java.lang.reflect.Constructor; public class Client { public static void main(String[] args) throws Exception { Singleton singleton1 = Singleton.getInstance(); Class<Singleton> clazz = Singleton.class; Constructor<Singleton> constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); Singleton singleton2 = constructor.newInstance(); System.out.println(singleton1 == singleton2); } }
使用序列化的破解与防御:
1 import java.io.Serializable; 2 3 public class Singleton implements Serializable { 4 5 private static final long serialVersionUID = -3230831923851678463L; 6 7 // 类初始化时立即创建对象 8 private static final Singleton instance = new Singleton(); 9 10 // 私有化构造器 11 private Singleton() { 12 } 13 14 public static Singleton getInstance() { 15 return instance; 16 } 17 18 // 防御:反序列化时,直接返回该方法的返回值 19 private Object readResolve() { 20 return instance; 21 } 22 23 }
调用示例:
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class Client { public static void main(String[] args) throws Exception { Singleton singleton1 = Singleton.getInstance(); File tempFile = new File("D:/test"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(tempFile)); oos.writeObject(singleton1); oos.close(); ObjectInputStream ios = new ObjectInputStream(new FileInputStream(tempFile)); Singleton singleton2 = (Singleton) ios.readObject(); ios.close(); System.out.println(singleton1 == singleton2); } }
2、懒汉式
实现方式一:
- 优点:实现懒加载,实例化对象是在调用getInstance()后
- 缺点:没有加锁synchronized,多线程使用下存在问题
1 /** 2 * 懒汉式方式一 3 */ 4 public class LazySingleton { 5 private static LazySingleton instance = null; 6 private LazySingleton(){} 7 public static LazySingleton getInstance(){ 8 if(instance == null){ 9 instance = new LazySingleton(); 10 } 11 12 return instance; 13 } 14 }
实现方式二:
- 改进:增加synchronized关键字,解决多线程问题
- 不足:synchronized锁住了这个对象,每次调用getInstance()都会对对象上锁,这样大大降低了性能,事实上只有在第一次instance为空时才需要加锁。
/** * 懒汉方式二 */ class LazySingleton2{ private static LazySingleton2 instance = null; private LazySingleton2(){} public static synchronized LazySingleton2 getInstance(){ if(instance == null){ instance = new LazySingleton2(); } return instance; } }
实现方式三(双重检测):
改进:对instance对了判断,只有当instance为空时才对对象进行加锁,提升性能。
不足:无序写入问题,如
a、线程1和2进入getInstance();
b、线程1首先进入synchronized线程同步,线程2等待线程1执行完成;
c、线程1判断instance为空则分配地址内存空间并实例化该对象;
d、线程1执行完成退出;
e、线程2进入synchronized同步,此时instance已被线程1是实例化,instance不为空,则返回线程1创建的instance实例。
由于JVM无序写入问题(指令重排),导致线程2有可能返回instance == null,如下
例如 instance = new Singleton() 可分解为如下伪代码:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
但是经过指令重排序后会变成这样
1 memory = allocate(); //1:分配对象的内存空间 2 instance = memory; //3:设置instance指向刚分配的内存地址 3 //注意,此时对象还没有被初始化! 4 ctorInstance(memory); //2:初始化对象
如:线程A 执行第4步采用先分配内存,然后将instance指向分配的内存,最后初始化,线程B在A线程执行初始化之前执行到第1步,发现非null,直接返回则最终出现了null。
这样就会出现问题,线程A执行了instance = memory(),这一步对线程B是可见,那么,线程B判断if(instance == null)时便会发现instance已经不为空了,便会返回instance,但是,由于instance只是指向了内存地址,并没有真正的初始化,那么线程B将会返回一个未能完全初始化的instance。
在JDK1.5之后,可以使用volatile变量禁止指令重排序
private static volatile LazySingleton2 instance = null; 1 /** 2 * 懒汉方式三 3 */ 4 class LazySingleton3{ 5 private static LazySingleton3 instance = null; 6 private LazySingleton3(){} 7 public static LazySingleton3 getInstance(){ 8 if(instance == null){// 1 9 synchronized (LazySingleton3.class){ //2 10 if(instance == null){//3 11 instance = new LazySingleton3(); //4 12 } 13 } 14 } 15 16 return instance; 17 } 18 }
实现方式四(静态内部类):
- 优点:懒加载策略,线程安全, 执行效率高。利用classloader加载机制实现初始化时只有一个线程,当LazySingleton4被加载时,instance不一定被初始化,因为SingleFactory没有被主动调用
- 缺点:若在构造行数中抛出异常,将得不到实例
1 /** 2 * 懒汉方式四 3 */ 4 class LazySingleton4{ 5 private LazySingleton4(){ 6 7 } 8 9 private static class SingleFactory{ 10 public static LazySingleton4 instance = new LazySingleton4(); 11 } 12 13 public static LazySingleton4 getInstance(){ 14 return SingleFactory.instance; 15 } 16 }
3、枚举实现
优点:线程安全,执行效率高,不能延迟加载
缺点:无法继承(这个其实也谈不上是缺点哈)
这么多种方式实现单例,如何选择呢?
- 单例对象占用资源少,不需要延时加载:枚举式好于饿汉式;
- 单例对象占用资源多,需要延时加载:静态内部类好于懒汉式。
1 public enum Singleton { 2 3 // 枚举本身就是单例 4 INSTANCE; 5 6 // 添加需要的方法 7 public void method() { 8 } 9 10 }
三、单例模式在Java中的应用及解读
Runtime是一个典型的例子,看下JDK API对于这个类的解释"每个Java应用程序都有一个Runtime类实例,使应用程序能够与其运行的环境相连接,可以通过getRuntime方法获取当前运行时。应用程序不能创建自己的Runtime类实例。",这段话,有两点很重要:
1、每个应用程序都有一个Runtime类实例
2、应用程序不能创建自己的Runtime类实例
只有一个、不能自己创建,是不是典型的单例模式?看一下,Runtime类的写法:
1 public class Runtime { 2 private static Runtime currentRuntime = new Runtime(); 3 4 /** 5 * Returns the runtime object associated with the current Java application. 6 * Most of the methods of class <code>Runtime</code> are instance 7 * methods and must be invoked with respect to the current runtime object. 8 * 9 * @return the <code>Runtime</code> object associated with the current 10 * Java application. 11 */ 12 public static Runtime getRuntime() { 13 return currentRuntime; 14 } 15 16 /** Don't let anyone else instantiate this class */ 17 private Runtime() {} 18 19 ... 20 }
以上就能看出Runtime使用getRuntime()方法并让构造方法私有保证程序中只有一个Runtime实例且Runtime实例不可以被用户创建。
还有以下在Java中的应用:
- Windows的Task Manager(任务管理器)。
- Windows的Recycle Bin(回收站)。
- 项目中,读取配置文件的类,一般只有一个对象,没必要每次创建。
- 数据库连接池。
- Application是单例的典型应用(Servlet编程)。
- Spring中,每个Bean默认是单例的。
- 每个Servlet是单例。
- Spring MVC中,控制器对象是单例的。