设计模式--单例
程序员文章站
2022-06-19 10:29:45
概述 单例模式(SingletonPattern),保证一个类仅有一个实例,并提供一个访问它的全局访问点。 单例模式有 3 个特点: 单例类只有一个实例对象; 该单例对象必须由单例类自行创建; 单例类对外提供一个访问该单例的全局访问点; 在很多比较大型的程序中,全局变量经常被用到。如果不用全局变量, ......
概述
单例模式(singletonpattern),保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式有 3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点;
在很多比较大型的程序中,全局变量经常被用到。如果不用全局变量,那么在使用到的模块中,都需要用参数将全局变量传入,这是非常麻烦的。虽然要减少使用全局变量,但是如果需要,还是要用。单例模式就是对传统的全局的一种改进。单例可以做到延时实例化,即在需要的时候才进行实例化。针对一些大型的类,延时实例化是有好处的。
实现
饿汉式单例
/** * 饿汉式单例 * 线程安全 */ public class singleton1 { // jvm保证在任何线程访问instance静态变量之前一定先创建了此实例 private static singleton1 instance = new singleton1(); // 私有化构造方法,保证外界无法直接实例化 private singleton1() { } // 提供全局访问点获取唯一的实例 public static singleton1 getinstance() { return instance; } }
- 优点:没有加锁,执行效率会提高。
- 缺点:类加载时就初始化,浪费内存。
- 场景:这种实现方式适合单例占用内存比较小,在初始化时就会被用到的情况。但是,如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。
懒汉式单例
/** * 双重检查单例(懒汉式) * 线程安全 * 单例实例在第一次使用时进行创建 */ public class singleton2 { private volatile static singleton2 instance = null; // 私有化构造函数 private singleton2() { } public static singleton2 getinstance() { if (instance == null) { // 多线程可达,可能存在a实例化释放锁后,阻塞在此的b获得同步锁,所以此处需要双重检测 synchronized (singleton2.class) { if (instance == null) { // 此处的执行顺序期望如下: // 1. memory = allocate() 分配对象的内存空间 // 2. ctorinstance() 初始化对象 // 3. instance = memory 设置instance指向刚分配的内存 // 如果不用volatile修饰变量, 2、3指令可能重排,导致获取未初始化的对象 instance = new singleton2(); } } } return instance; } }
- 优点:第一次调用才初始化,避免内存浪费。
- 缺点:必须加锁synchronized才能保证单例,(静态同步方法实现的懒汉式)加锁会影响效率。
登记式单例
/** * 静态内部类单例(登记式、延迟加载) */ public class singleton3 { private singleton3() { } /** * 静态内部类 * 在第一次调用getinstance方法之前,singletonwrapper类是没有被加载的,因为它是一个静态内部类。 * 当有线程第一次调用getinstance的时候,singletonwrapper就会被class loader加载进jvm,在加载的同时,执行instance的初始化。 * 所以,这种写法,仍然是一种懒汉式的单例类。 */ private static class singletonwrapper { private static final singleton3 instance = new singleton3(); } /** * 为什么这样写就是线程安全的呢? * 因为类的加载的过程是单线程执行的。它的并发安全是由jvm保证的。 * 所以,这样写的好处是在instance初始化的过程中,由jvm的类加载机制保证了线程安全, * 而在初始化完成以后,不管后面多少次调用getinstance方法都不会再遇到锁的问题了。 * * @return */ public static singleton3 getinstance() { return singletonwrapper.instance; } }
- 优点: 内部类只有在外部类被调用才加载,产生singleton实例;又不用加锁。此模式有上述两个模式的优点,屏蔽了它们的缺点,是推荐的单例模式。
- 缺点: 在实例需要序列化的场景下,反射和序列化会破坏单例,这是懒汉式、饿汉式和登记式共同存在的缺陷。
枚举单例
/** * 枚举单例 * 线程安全 */ public class singleton4 { // 私有构造函数 private singleton4() { } public static singleton4 getinstance() { return singleton.instance.getinstance(); } // 枚举实例是static final类型的,也就表明只能被实例化一次。 // 在调用构造方法时,我们的单例被实例化 private enum singleton { instance; private singleton4 singleton; // jvm保证这个方法绝对只调用一次 singleton() { singleton = new singleton4(); } public singleton4 getinstance() { return singleton; } } }
- 枚举提供了序列化机制,推荐的
最佳实现方式
反射和反序列化对单例的影响
通过反射来实例化类
/** * 用反射来获得实例 */ public class singleton5 { public static void main(string[] args) throws nosuchmethodexception, illegalaccessexception, invocationtargetexception, instantiationexception { class<singleton1> clz = singleton1.class; constructor<singleton1> constructor = clz.getdeclaredconstructor(); constructor.setaccessible(true); singleton1 reflectinstance = constructor.newinstance(); singleton1 instance = singleton1.getinstance(); system.out.println(reflectinstance == instance); // false } }
结果输出false,说明reflectinstance和instance不是同一个对象。(==比较的是实例对象的内存地址)
通过反序列化来实例化类
/** * 反序列化来获得实例 */ public class singleton6 { public static void main(string[] args) throws ioexception, classnotfoundexception { // 单例(此处对单例进行修改,实现serializable接口) singleton1 singleton = singleton1.getinstance(); // 序列化 fileoutputstream fos = new fileoutputstream("singleton1.obj"); objectoutputstream oos = new objectoutputstream(fos); oos.writeobject(singleton); oos.flush(); fos.close(); oos.close(); // 反序列化 fileinputstream fis = new fileinputstream("singleton1.obj"); objectinputstream ois = new objectinputstream(fis); singleton1 instance = (singleton1) ois.readobject(); fis.close(); ois.close(); // 对比 system.out.println(singleton == instance); // false } }
结果输出false,说明singleton和instance指向不同对象。
如何避免单例被破坏
修改单例类,解决反序列化的问题
/** * 饿汉式单例 * 线程安全 */ public class singleton1 implements serializable { // jvm保证在任何线程访问instance静态变量之前一定先创建了此实例 private static singleton1 instance = new singleton1(); // 私有化构造方法,保证外界无法直接实例化 private singleton1() { } // 提供全局访问点获取唯一的实例 public static singleton1 getinstance() { return instance; } //该方法在反序列化时会被调用,该方法不是接口定义的方法,有点儿约定俗成的感觉 protected object readresolve() throws objectstreamexception { system.out.println("调用了readresolve方法!"); return instance; } }
结果输出
调用了readresolve方法! true
应用场景
单例模式可以避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间。有以下场景的特点即可使用单例。
当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如数据库的连接池、zk分布式锁、工具类等。
当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
公众号 【当我遇上你】