设计模式:单例模式
一.
单例模式指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
二.饿汉模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class HungrySingleton { private HungrySingleton(){} private final static HungrySingleton HUNGRY_SINGLETON ; static { HUNGRY_SINGLETON = new HungrySingleton(); } public static HungrySingleton getInstance(){ return HUNGRY_SINGLETON; } } |
饿汉式是指类的被加载的时候,就被初始化,并创建单例对象,不会存在访问安全问题。
缺点:所有的饿汉单例对象都会在项目启动时初始化,会造成大量内存资源浪费。
三.懒汉模式
(1)考虑到饿汉模式的缺点后,加以修改,在类被调用的时候,才初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class LazySimpleSingleton { private LazySimpleSingleton(){} private static LazySimpleSingleton LAZY_SIMPLE_SINGLETON = null ; public static LazySimpleSingleton getIn,stance(){ if ( null ==LAZY_SIMPLE_SINGLETON){ LAZY_SIMPLE_SINGLETON = new LazySimpleSingleton(); return LAZY_SIMPLE_SINGLETON; } return LAZY_SIMPLE_SINGLETON; } } |
但是会带来一个新的问题,就是在多线程环境下,有两个线程同一时间进入getInstance方法,同事满足null=LAZY_SIMPLE_SINGLETON时,会创建两个对象,然后后创建的会覆盖先创建的单例对象。
考虑到这个问题后,进一步优化,使用synchronized关键字,给方法加锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class LazySimpleSingleton { private LazySimpleSingleton(){} private static LazySimpleSingleton LAZY_SIMPLE_SINGLETON = null ; public synchronized static LazySimpleSingleton getInstance(){ if ( null ==LAZY_SIMPLE_SINGLETON){ LAZY_SIMPLE_SINGLETON = new LazySimpleSingleton(); return LAZY_SIMPLE_SINGLETON; } return LAZY_SIMPLE_SINGLETON; } } |
当一个线程调用getInstance方法时,另一个线程也调用该方法,会出现阻塞,直到第一个线程执行结束,才继续调用,完美解决了线程安全问题。
出现新的问题:
如果线程数量暴增,给getInstatnce加锁,只有一个线程运行该方法,其他线程全部阻塞等待,用户体验不好。新的解决方案--双重检查锁单例写法应运而生。
(2)双重检查锁单例 (进门安检一次,闸口再检查一次)
改造一下写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class LazyDoubleSingleton { private volatile static LazyDoubleSingleton instance; private LazyDoubleSingleton(){} public static LazyDoubleSingleton getInstance(){ synchronized (LazyDoubleSingleton. class ){ if ( null ==instance){ instance = new LazyDoubleSingleton(); } } return instance; } } |
这样的写法,其实和上一种写法差不多,都会造成大量线程阻塞。那如果把if条件往上升一级呢,先判断,再加锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class LazyDoubleSingleton { private volatile static LazyDoubleSingleton instance; private LazyDoubleSingleton(){} public static LazyDoubleSingleton getInstance(){ if ( null ==instance){ synchronized (LazyDoubleSingleton. class ){ instance = new LazyDoubleSingleton(); } } return instance; } } |
经过此次修改后,还是会出现线程安全问题。
因为当两个线程同事满足null==instance条件后,会执行sychronized代码块的代码,该对象还是会被创建两次。
再优化一下,我们在sychronized代码块中再进行对象的非空检查,这样该对象就不会被创建两次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class LazyDoubleSingleton { private volatile static LazyDoubleSingleton instance; private LazyDoubleSingleton(){} public static LazyDoubleSingleton getInstance(){ //检查是否要阻塞 if ( null ==instance){ synchronized (LazyDoubleSingleton. class ){ //检查是否要创建对象 if ( null ==instance){ instance = new LazyDoubleSingleton(); } } } return instance; } } |
四.静态内部类单例的写法
双重检查锁单例这种方式虽然解决了线程安全问题和性能问题,但是用到了sychronized总是要上锁,对性能还是有一些影响。我们可以采用静态内部类的方式进行优化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /** * @Author wen.jie * @Description 使用InnerClassSingleton类时,会默认先初始化内部类,如果没有使用,则内部类不初始化 **/ public class InnerClassSingleton { private InnerClassSingleton(){} public static InnerClassSingleton getInstance(){ return InnerClass.INNER_CLASS_SINGLETON; } private static class InnerClass{ private static final InnerClassSingleton INNER_CLASS_SINGLETON = new InnerClassSingleton(); } } |
这种方式兼顾了饿汉单例写法的内存浪费问题和sychronized的性能问题,内部类一定要在方法调用之前就被初始化,巧妙的避开了线程安全问题。静态内部类单例写法真的完美了吗?
如果我们用反射强行创建对象呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static void main(String[] args) { try { Constructor<InnerClassSingleton> constructor = InnerClassSingleton. class .getDeclaredConstructor( null ); constructor.setAccessible( true ); InnerClassSingleton o1 = constructor.newInstance(); InnerClassSingleton o2 = constructor.newInstance(); System.out.println(o1==o2); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } |
发现创建了两个不同的实例。优化方法,在构造器中,进行判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class InnerClassSingleton { private InnerClassSingleton(){ if (InnerClass.INNER_CLASS_SINGLETON!= null ){ throw new RuntimeException( "不允许创建多个实例" ); } } public static InnerClassSingleton getInstance(){ return InnerClass.INNER_CLASS_SINGLETON; } private static class InnerClass{ private static final InnerClassSingleton INNER_CLASS_SINGLETON = new InnerClassSingleton(); } } |
当我们再用反射去创建对象时,就会抛出异常。
但这种写法值得斟酌,在构造器中抛出异常,不够优雅,有没有比静态内部类更优雅的单例写法呢?
五.枚举式单例写法(jdk1.5以后)
标准写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public enum EnumSingleton { INSTANCE; private Object data; public Object getData(){ return data; } public void setData(Object data){ this .data = data; } public static EnumSingleton getInstance(){ return INSTANCE; } } |
这种写法更加简洁,无偿提供序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现单例的最佳方法。(Effective Java原文)
下面对其测试:
对该单例对象进行序列化和反序列化测试
public static void main(String[] args) {
try {
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
FileOutputStream fos = new FileOutputStream("EnumSingleton.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("EnumSingleton.dat");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton)ois.readObject();
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData()==instance2.getData());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
测试结果:
六.反序列化破坏单例模式
反序列化会从磁盘文件中读取对象,转为内存对象,反序列化的对象会重新分配内存,如果反序列化的对象是单例对象,则违背了单例模式的初衷。
下面对之前写过的双重检查锁单例对象进行序列化和反序列化(先实现Serializable接口):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public static void main(String[] args) { try { LazyDoubleSingleton instance1 = null ; LazyDoubleSingleton instance2 = LazyDoubleSingleton.getInstance(); FileOutputStream fos = new FileOutputStream( "LazyDoubleSingleton.dat" ); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(instance2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream( "LazyDoubleSingleton.dat" ); ObjectInputStream ois = new ObjectInputStream(fis); instance1 = (LazyDoubleSingleton)ois.readObject(); System.out.println(instance1==instance2); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } |
结果:
那我们如何保证序列化的情况下也能够实现单例模式呢?
其实只需要在单例对象中增加一个readResolve()方法(注意返回值必须是Object类型,不能是单例对象类型):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class LazyDoubleSingleton implements Serializable { private volatile static LazyDoubleSingleton instance; private LazyDoubleSingleton(){} public static LazyDoubleSingleton getInstance(){ //检查是否要阻塞 if ( null ==instance){ synchronized (LazyDoubleSingleton. class ){ //检查是否要创建对象 if ( null ==instance){ instance = new LazyDoubleSingleton(); } } } return instance; } private Object readResolve(){ return instance; } } |
再看运行结果,就是true了
具体原理是在readObject方法中判断了反序列化的对象中是否有无参的readResolve()方法(jdk源码中)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix