设计模式-02.单例模式
单例模式
学习途径来自菜鸟教程:https://www.runoob.com/design-pattern/singleton-pattern.html
单例模式(Singleton pattern),这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
- 属于创建型模式,它提供了一种创建对象的最佳方式。
- 在任何情况下都只有一个实例。
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
**主要解决:**一个全局使用的类频繁地创建与销毁。
**何时使用:**控制实例数目,节省系统资源的时候。
**如何解决:**判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
**关键代码:**构造函数是私有的。
重 点:
- 私有化构造器
- 了解线程对单例模式的影响,保证线程安全
- 延迟加载,避免类加载时就创建过多的无用单例从而造成内存浪费
- 防止序列化和反序列化破坏单例
- 防止反射对单例的破坏
优点
- 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 2、避免对资源的多重占用(比如写文件操作)。
缺点
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
饿汉式单例
- 写法1:
public class HungrySingleton{
public static final HungrySingleton hungrySingleton = new HungrySingleton();
//构造方法私有化
private HungrySingleton(){}
public static HungrySingleton getInstance(){return hungrySingleton;}
}
- 写法2:
public class HungrySingleton{
//加载顺序
//先静态后动态
//先上后下
//先属性后方法
public static final HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton()
}
//构造方法私有化
private HungrySingleton(){}
public static HungrySingleton getInstance(){return hungrySingleton;}
}
优点
- 没有加锁,执行效率会提高。
- 线程安全(基于 classloader 机制避免了多线程的同步问题)
缺点
- 类加载时就初始化,(某些情况下)浪费内存。
懒汉式
1. 线程不安全
public class LazySimpleSingleton{
public static LazySimpleSingleton instance;
//构造方法私有化
private LazySimpleSingleton(){}
public static LazySimpleSingleton getInstance(){
if(instance == null){
//多线程情况下,可能有多个线程同时进入instance == null的条件,从而导致创建多个实例
instance = new LazySimpleSingleton();
}
return instance;
}
}
优点
- 延时加载,节省空间
缺点
- 线程不安全
2. 线程安全
public class LazySimpleSingleton{
public static LazySimpleSingleton instance;
//构造方法私有化
private LazySimpleSingleton(){}
//加锁 syncronized 关键字
//多线程下此处容易阻塞,其他线程在此处等待
public synchronized static LazySimpleSingleton getInstance(){
if(instance == null){
instance = new LazySimpleSingleton();
}
return instance;
}
}
优点
- 延时加载,节省空间
- 线程安全
缺点
- 效率低
3. 双检锁/双重校验锁(DCL)
双检锁/双重校验锁(DCL,即 double-checked locking)
public class LazyDoubleCheckSingleton{
//volatile关键字,解决指令重排序问题
public volatile static LazyDoubleCheckSingleton instance;
//构造方法私有化
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
//判断是否需要阻塞
if(instance == null){
//锁,多线程下可能会有部分线程进入此处
synchronized(LazyDoubleCheckSingleton.class){
//判断是否需要创建实例
if(instance == null){
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
注:jdk1.5起
优点
- 延时加载,节省空间
- 线程安全
- 效率高
缺点
- 实现较复杂
- 代码可读性较低
4. 登记式/静态内部类
/**
* 内部类加载方式及顺序:
* ClassPath: LazyStaticInnerSingleton.class
* LazyStaticInnerSingleton.class$LazyHolder.class
* 优点:写法优雅,利用了java本身语法的特点,性能高,避免了内存浪费
* 缺点:
*/
public class LazyStaticInnerSingleton {
//构造方法私有化
private LazyStaticInnerSingleton(){
//防止被反射破坏
//if(LazyHolder.INSTANCE != null){
// throw new RuntimeException("不允许非法访问")
//}
}
public static LazyStaticInnerSingleton getInstance(){
//程序中调用getInstance时发现调用了LazyHolder内部类,
//去加载LazyStaticInnerSingleton.class$LazyHolder.class
//执行完LazyStaticInnerSingleton.class$LazyHolder.class的逻辑之后返回结果
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
优点
- 延时加载,节省空间
- 线程安全
缺点
- 能够被反射破坏
5. 枚举式单例
枚举式单例是注册式单例的一种。
public enum EnumSingleton {
INSTANCE;
private Object ojb;
public Object getObj(){return obj;}
public void setObj(Object obj){ this.ojb = obj;}
public static EnumSingleton getInstance(){return INSTANCE;}
}
优点
-
线程安全
同饿汉式类似,在声明时便初始化好,不存在线程问题
-
不被反射破坏
public class ReflectTest{ Class<?> clazz = LazyStaticInnerSingleton.class; //枚举类Enum没有无参的构造方法,通过访问构造方法获取实例 Constructor c = clazz.getDeclaredConstructor(String.class, int.class); //构造方法为私有,设置“强吻” c.setAccessible(true); //此行会报错,Cannot reflectively create enum objects(不能通过反射创建枚举对象) Object instance1 = c.newInstance(); System.out.println(instance1); }
Construcor.java if(clazz.getModifiers() & Modifier.ENUM != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects");
缺点
- 不是延时加载
- 不适合大批量创建单例的情况
6. 容器式单例
public class ContainerSingleton{
private ContainerSingleton(){};
public static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className){
Object instance = null;
if(!ioc.containsKey(className)) {
synchronized(ioc){
if(!ioc.containsKey(className)){
try{
instance = Class.forName(className).newInstance();
ioc.pub(className, instance);
}catch(Exception e){
e.printStackTrace();
}
return instance;
}
}
}else{
//此处简单实现,这里还应该判断 ioc.get(className) == null
//或者ioc.get(className) == new Object()的情况,
//这两种情况下应该都需要重新ioc.pub(className, instance)
return ioc.get(className);
}
}
}
反射破坏单例
public class ReflectTest{
Class<?> clazz = LazyStaticInnerSingleton.class;
//通过访问无参的构造方法获取实例
Constructor c = clazz.getDeclaredConstructor(null);
//构造方法为私有,设置“强吻”
c.setAccessible(true);
Object instance1 = c.newInstance();
Object instance2 = c.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
}
解决:可参考5.枚举式单例,或4.登记式单例代码中注释部分,对单例的私有方法进行判断改造
序列化破坏单例
public class SeriableSingleton implements Serializable {
//序列化
//把内存中对象的状态转换为字节码的形式
//把字节码通过IO输出流,写到磁盘上
//永久保存下来,持久化
//反序列化
//将持久化的字节码内容,通过IO输入流读到内存中来
//转化成一个Java对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
//防止单例模式被序列化破坏,此处也用到了桥接模式
private Object readResolve(){ return INSTANCE;}
ThreadLocalSingleton
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance(){
return threadLocaLInstance.get();
}
}
经验之谈
- 一般情况下,不建议使用第 1 种—(线程不安全)和第 2 种—(线程安全)懒汉方式。
- 建议使用饿汉方式。
- 只有在要明确实现 lazy loading 效果时,才会使用第 4 种–登记方式。
- 如果涉及到反序列化创建对象时,可以尝试使用第 5 种—枚举方式。
- 如果有其他特殊的需求,可以考虑使用第 3 种—双检锁方式。