介绍
单例模式是GOF23中最简单的模式。单例的设计模式里的出场率很高,它简单但也有多种实现方式,也正是因为它的灵活性和重要性使唤其多次出现在面试笔试中,经常与工厂模式搭配使用。
作用
保证一个类只能有一个实例。多次创建只会返回同一个实例。
作用范围
- 如word中的工具箱,任何时候只能有一个工具箱。(大话设计模式)
- window的任务管理器等等
- 网站的计数器
周围的单例:spring bean的创建就是单例+工厂实现
实现步骤
- 私有化类的所有构造方法,使调用者不可直接通过new创建该类实例
- 单例类中暴露一个名为getInstance()的静态方法来为调用者返回一个实例对象
单例的五种实现
1. 饿汉式
/**
* 单例模式之饿汉式
*/
public class Singleton1 {
//私有化构造方法
private Singleton1(){};
//在类初始化时就创建类的实例
private static Singleton1 singleton = new Singleton1();
//对外暴露的方法获取单例对象
public static Singleton1 getInstance(){
return singleton;
}
}
优点:获取实例效率高
缺点:如果存在初始化类后并没有获取使用单例对象这种情况就会造成内存的浪费。
2. 懒汉式
/**
* 单例模式之懒汉式
*/
public class Singleton2 {
//私有化构造方法
private Singleton2(){};
//此处并不初始化单例对象
private static Singleton2 singleton= null;
//对外暴露的方法获取单例对象
public synchronized static Singleton2 getInstance(){
if(singleton==null){
singleton = new Singleton2();
}
return singleton;
}
}
特点:在类初始化时不创建实例,在调用者第一次调用getInstance方法时才创建实例。
优点:比起饿汉式避免了内存的浪费,线程安全
缺点:为避免多线程环境下创建出多个单例对象,采用同步机制,导致懒汉式多线程环境下的效率低下。
多线程破坏单例的情况为:多个线程一同调用getInstance方法(且均为第一次调用),并一同以singleton==null进入了if语句块,线程1进入同步代码块,线程2在外等待线程1执行结束。此时单例对象已经存在,待线程1创建对象后出来,线程2会继续创建对象,破坏了单例模式。
3. 静态内部类
/**
* 单例模式之静态内部类
*/
public class Singleton3 {
//静态内部类
private static class SingletonHolder{
private static Singleton3 singleton = new Singleton3();
}
public static Singleton3 getInstance(){
return SingletonHolder.singleton;
}
}
优点
- 效率高
- 线程安全
- 类初始化时并不初始化其内部类,只有在调用getInstance方法时才会初始化内部类,实现懒加载。
4. 枚举
/**
* 单例模式之静态内部类
*/
public enum Singleton4 {
SINGLETON;
}
优点:天然线程安全,由JVM底层保障。并且避免反射反序列化破解单例的漏洞。
缺点:无延时加载。
5. 双重锁定
/**
* 单例模式之双重锁
*
*/
public class Singleton5 {
//私有化构造方法
private Singleton5(){};
//在类初始化时并不创建实例
private static volatile Singleton5 singleton = null;
//对外暴露的方法获取单例对象
public static Singleton5 getInstance(){
if(singleton==null){
synchronized (Singleton5.class) {
if(singleton==null){
singleton = new Singleton5();
}
}
}
return singleton;
}
}
特点:因为双重锁定使用先判断后进同步代码块的方式,比起饿汉式效率高了一点,先判断单例对象是否为null,为null才进入同步。
在JDK1.5前因没有引入volatile关键字,singleton = new Singleton5()此行代码会分解出如下三行伪代码。
1 memory=allocate();// 分配内存 相当于c的malloc
2 ctorInstanc(memory) //初始化对象
3 instance=memory //设置instance指向刚分配的地址
伪代码的排序顺序有可能是1-2-3,也有可能是1-3-2。在多线程环境下假设线程A的执行顺序为1-3-2,在执行完3代码后被挂起,线程B来判断instance不为null会直接返回,出现instance没有初始化的情况,会导致程序出错。故JDK1.5前不推荐使用。
注:除枚举式外所有的单例模式都会被反射和反序列化所破解为多个实例对象。在此给出防止破解的方法
- 反射:
/**
* 反射破解单例模式
*/
public class Test {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
Class<Singleton1> clazz = (Class<Singleton1>) Class.forName("pengfei.guo.singleton.Singleton1");
Constructor<Singleton1> c = clazz.getDeclaredConstructor(null);
//设置可访问私有的成员
c.setAccessible(true);
Singleton1 s1 = c.newInstance();
Singleton1 s2 = c.newInstance();
System.out.println(s1==s2);
}
}
解决方案:在构造方法中加入如下语句,在反射第二次通过调用构造方法创建实例时,singleton!=null,这时就手动抛出异常来阻止单例对象的创建。
private Singleton5(){
if(singleton!=null){
throw new RuntimeException();
}
};
2.反序列化:
/**
* 反序列化破解单例模式
*/
public class Test {
public static void main(String[] args) {
try {
Singleton1 s1 = Singleton1.getInstance();
System.out.println(s1);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:/aa.txt"));
oos.writeObject(s1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:/aa.txt"));
Singleton1 s3 =(Singleton1) ois.readObject();
System.out.println(s3);
ois.close();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
此时输出结果s1与s3不为同一个对象。解决办法就是在单例类中添加一个方法
private Object readResolve(){
return singleton;
}
在对象的反序列化时会调用此方法,直接返回singleton对象就可防止这次现象发生。
总结
单例模式的几种实现并没有优劣,只有在适应的场景选择适合的实现。
在需要懒加载的情况 静态内部类 优于 懒汉式
不需要懒加载内存充裕 枚举式 优于 饿汉式