更多最新文章欢迎大家访问我的个人博客:smile::豆腐别馆
在某些系统中,为了节省内存资源,保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
一、模式的定义
单例模式(Singleton),指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容不一致等错误。
在计算机系统中,还有Windows的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、web应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
单例模式有三个特点: (1)单例类只有一个实例对象。 (2)该单例对象必须由单例类自行创建。 (3)单例类对外提供一个访问该单例的全局访问点。
二、模式的实现 单例模式通常有两种实现形式。
1. 懒汉式单例 (1)示例代码 以项目中常见的线程池为例,来创建一个懒汉式的单例线程池:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package com.yls.cloud.product.utils;import com.yls.cloud.product.dto.constant.ThreadPoolConstant;import java.util.concurrent.Executors;import java.util.concurrent.LinkedBlockingDeque;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;public class LazySingleton { private static volatile ThreadPoolExecutor threadPool = null ; private LazySingleton () { } public static ThreadPoolExecutor getInstance () { if (threadPool == null ) { synchronized (LazySingleton.class) { if (threadPool == null ) { threadPool = new ThreadPoolExecutor(ThreadPoolConstant.CORE_POOL_SIZE, ThreadPoolConstant.MAX_POOL_SIZE, ThreadPoolConstant.KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(ThreadPoolConstant.BLOCKING_QUEUE_SIZE), Executors.defaultThreadFactory() ); } } } return threadPool; } public void execute (Runnable runnable) { if (runnable == null ) { return ; } threadPool.execute(runnable); } public void cancel (Runnable runnable) { if (threadPool != null ) { threadPool.getQueue().remove(runnable); } } }
(2)优缺点 我们主要关心上述代码中的getInstance()
方法,阅读代码我们可以清楚地看到该模式的特点,即类加载时并没有生成单例,而是当程序第一次调用getInstance()
方法时才会去创建这个实例。顾名思义通俗点讲就是比较懒,要用到了我再创建,没用到我就不创建。
缺点 : ① 这就是典型的时间换空间 ,也就是每次获取实例都会进行判断,看看是否需要创建实例,这样显然就浪费了每次判断的时间。 ② 同时我们可以看到为了保证多线程下的线程安全问题,我们使用了volatile
及synchronized
关键字,这样线程安全问题确实可以得到保障,但是每次访问时都需要同步,这样就会影响性能,且会消耗更多的资源。
优点 : 当然,时间换空间 也有好处,如果一直没有人使用的话,那就不会创建实例,则可以节约内存空间。这也就是懒汉式单例的优点。
2. 饿汉式单例 (1)示例代码 依旧以线程池为例,来创建一个饿汉式的单例线程池:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.yls.cloud.product.utils;import com.yls.cloud.product.dto.constant.ThreadPoolConstant;import java.util.concurrent.Executors;import java.util.concurrent.LinkedBlockingDeque;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;public class HungrySingleton { private static final ThreadPoolExecutor threadPool = new ThreadPoolExecutor( ThreadPoolConstant.CORE_POOL_SIZE, ThreadPoolConstant.MAX_POOL_SIZE, ThreadPoolConstant.KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(ThreadPoolConstant.BLOCKING_QUEUE_SIZE), Executors.defaultThreadFactory()); private HungrySingleton () { } public static ThreadPoolExecutor getInstance () { return threadPool; } public void execute (Runnable runnable) { if (runnable == null ) { return ; } threadPool.execute(runnable); } public void cancel (Runnable runnable) { if (threadPool != null ) { threadPool.getQueue().remove(runnable); } } }
(2)优缺点 同样的,我们主要关心上述代码中的getInstance()
方法,阅读代码我们同样可以清楚地看到该模式的特点,即在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。
3. Holder模式单例(静态内部类) Holder持有者单例模式,相比较于懒汉与饿汉似乎出现在视野中的频率会少些,但是这种模式在实际工作中用到的频率反而要高于前面两者。为什么呢?因为它既结合了饿汉模式的线程安全性,又结合了懒汉式的懒加载。同时也不需要使用synchronized
关键字,所以性能也有所保证。好了,话不多说,依旧以创建单例线程池为例,上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package com.yls.cloud.product.utils;import com.yls.cloud.product.dto.constant.ThreadPoolConstant;import java.util.concurrent.Executors;import java.util.concurrent.LinkedBlockingDeque;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;public class HolderSingleton { private HolderSingleton () { } private static class CreateThreadPool { private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor( ThreadPoolConstant.CORE_POOL_SIZE, ThreadPoolConstant.MAX_POOL_SIZE, ThreadPoolConstant.KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(ThreadPoolConstant.BLOCKING_QUEUE_SIZE), Executors.defaultThreadFactory()); } public static ThreadPoolExecutor getInstance () { return CreateThreadPool.threadPool; } public void execute (Runnable runnable) { if (runnable == null ) { return ; } getInstance().execute(runnable); } public void cancel (Runnable runnable) { if (getInstance() != null ) { getInstance().getQueue().remove(runnable); } } }
4. 枚举式单例 终于到了最后一种模式了,是的,此种模式正是被大家推崇为最优实现单例模式的方式 - - 枚举式单例模式。为什么说是大家都推崇的呢?我罗列下此种方式的好处,相信你也会推崇它:
首先当然是写法简单,因为自从有了它,你将不需要再去考虑我要怎样添加各种关键字如volatile
、static
、final
,也不需要再去考虑如何写好内部类,更不用再担心万一关键字加错后会发生何种灾难性后果,你只需要在普通方法里写好自己的业务代码即可,堪称无脑万金油。
利用了枚举的特性来保证线程的安全。
利用了枚举的特性防止反射强行调用构造方法 。
依旧利用了枚举的特性,利用枚举提供的自动序列化机制,从而防止反序列化的时候会去创建新的对象。
其它模式都存在反射调用 及反序列化 破坏单例弊端。(下文会提到该问题及解决办法)
老规矩,创建单例线程池,上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package com.yls.cloud.doufuplus.pattern.singleton;import com.yls.cloud.doufuplus.pattern.singleton.constant.SingletonConstant;import java.util.concurrent.Executors;import java.util.concurrent.LinkedBlockingDeque;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;public enum EnumSingleton { INSTANCE; private ThreadPoolExecutor threadPoolExecutor; private EnumSingleton () { threadPoolExecutor = new ThreadPoolExecutor(SingletonConstant.CORE_POOL_SIZE, SingletonConstant.MAX_POOL_SIZE, SingletonConstant.KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(SingletonConstant.BLOCKING_QUEUE_SIZE), Executors.defaultThreadFactory()); } public ThreadPoolExecutor getInstance () { return threadPoolExecutor; } public static ThreadPoolExecutor getPool () { return EnumSingleton.INSTANCE.getInstance(); } public void execute (Runnable runnable) { if (runnable == null ) { return ; } INSTANCE.getInstance().execute(runnable); } public void cancel (Runnable runnable) { if (INSTANCE.getInstance() != null ) { INSTANCE.getInstance().getQueue().remove(runnable); } } public static void main (String[] args) { ThreadPoolExecutor instance1 = EnumSingleton.INSTANCE.getInstance(); ThreadPoolExecutor instance2 = EnumSingleton.INSTANCE.getInstance(); System.out.println(instance1); System.out.println(instance2); System.out.println(instance1 == instance2); ThreadPoolExecutor pool1 = EnumSingleton.getPool(); ThreadPoolExecutor pool2 = EnumSingleton.getPool(); System.out.println(pool1); System.out.println(pool2); System.out.println(pool1 == pool2); System.out.println(instance1 == pool1); } }
三、单例竟被破坏? 注:下述两种破坏情况并不适用于枚举式的单例(枚举的特性已经帮助我们解决了下述问题)
1. 反射破解单例 我们知道Java的访问控制是停留在编译层的,也就是它并不会在class文件中保留下任何痕迹,只有在编译的时候进行访问控制的检查。而我们却是可以通过反射的手段来访问类中的成员,比如致命的:私有构造方法。
(1)示例代码 首先先编写个反射用例,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.yls.cloud.product.utils;import java.lang.reflect.Constructor;import java.util.concurrent.ThreadPoolExecutor;public class ReflectTest { public static void main (String[] args) throws Exception { ThreadPoolExecutor instance1 = LazySingleton.getInstance(); ThreadPoolExecutor instance2 = LazySingleton.getInstance(); System.out.println("破解前:" + instance1); System.out.println("破解前:" + instance2); System.out.println(instance1 == instance2); Constructor<LazySingleton> con = LazySingleton.class.getDeclaredConstructor(); con.setAccessible(true ); LazySingleton lazySingleton1 = con.newInstance(); LazySingleton lazySingleton2 = con.newInstance(); System.out.println("破解后:" + lazySingleton1); System.out.println("破解后:" + lazySingleton2); System.out.println(lazySingleton1 == lazySingleton2); } }
让我们运行看看结果:
(2)破坏原因 惊不惊喜,意不意外?反射后对象竟然变了。我们知道单例模式的目标是,任何时候该类都只有唯一的一个对象,但通过setAccessible(true)
执行反射的对象后,在使用时已经取消了Java语言的访问检查,使得原本该私有的构造函数也能够被外部访问到了,从而使得单例模式失效。
(3)解决办法 如果要抵御这种攻击,就要防止构造函数被成功调用超过一次。因此可以在构造函数中对实例化次数进行统计,大于一次我们就抛出异常。因此我们需将原私有构造代码改造如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private static int count = 0 ;private LazySingleton () { synchronized (LazySingleton.class) { if (count > 0 ) { throw new RuntimeException("被创建了超过一个实例!当前单例已被侵犯!" ); } count++; } }
改造好后,我们再运行上文的反射调用代码,就可以看到已经被成功拦截掉了:
2. 序列化破解单例 (1)示例代码 序列化及反序列化测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.yls.cloud.product.utils;import java.io.*;import java.util.concurrent.ThreadPoolExecutor;public class SerializeTest { public static void main (String[] args) throws IOException, ClassNotFoundException { test(); } private static void test () throws IOException, ClassNotFoundException { SimpleHungrySingleton instance = SimpleHungrySingleton.getInstance(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos); objectOutputStream.writeObject(instance); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream objectInputStream = new ObjectInputStream(bais); SimpleHungrySingleton newInstance = (SimpleHungrySingleton) objectInputStream.readObject(); baos.close(); bais.close(); System.out.println(instance == newInstance); } }
上文的代码由于是生成的线程池,导致跑测试代码时,序列化反序列化错误,为方便直接新写一个简单测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.yls.cloud.product.utils;import java.io.Serializable;public class SimpleHungrySingleton implements Serializable { private final static SimpleHungrySingleton instance; static { instance = new SimpleHungrySingleton(); } private SimpleHungrySingleton () { } public static SimpleHungrySingleton getInstance () { return instance; } }
(2)破坏原因 我们运行上面的测试代码即可知道结果是等于false的,可是这是为什么呢? 其实说到底依旧是反射在作祟,因为序列化会通过反射来调用无参数的构造方法,从而创建了一个新的对象。
(3)解决办法 知道了原因,那么我们要如何解决呢?其实我们只需要往我们的单例类里加入一个readResolve()
方法即可,完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.yls.cloud.product.utils;import java.io.Serializable;public class SimpleHungrySingleton implements Serializable { private final static SimpleHungrySingleton instance; static { instance = new SimpleHungrySingleton(); } private SimpleHungrySingleton () { } public static SimpleHungrySingleton getInstance () { return instance; } private Object readResolve () { return instance; } }
再次运行序列化测试类,结果便为true了。至于为什么添加了这个方法就可以抵御住反序列化带来的破坏,可参考该篇文章单例模式的攻击之序列化与反序列化 带来的解读。
四、模式的扩展 单例模式可扩展为有限的多例(Multitcm) 模式,这种模式可生成有限个实例并保存在 ArrayList 中,当需要时则可以随机获取。 这样的好处是我们可以决定内存中存在有多少个实例 ,可以修正单例模式带来的性能问题。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 package com.yls.cloud.product.utils;import java.io.Serializable;import java.util.ArrayList;import java.util.Random;public class SingletonExtend implements Serializable { private static int countCall = 0 ; private static int maxObj = 3 ; private static ArrayList<SingletonExtend> instances = new ArrayList<SingletonExtend>(); private static ArrayList<String> names = new ArrayList<String>(); private static int number; private SingletonExtend () { synchronized (SingletonExtend.class) { if (countCall > 0 ) { throw new RuntimeException("被创建了超过一个实例!当前单例已被侵犯!" ); } countCall++; } } private SingletonExtend (String name) { names.add(name); } private Object readResolve () { return instances; } static { for (int x = 0 ; x < maxObj; x++) { instances.add(new SingletonExtend("doufuplus" + x)); } } public static SingletonExtend getInstance () { Random r = new Random(); number = r.nextInt(2 ); return instances.get(number); } public void hello () { System.out.println("Hello:" + names.get(number)); } public static void main (String[] args) { for (int i = 0 ; i < 5 ; i++) { SingletonExtend instance = SingletonExtend.getInstance(); instance.hello(); } } }
运行结果如下:
五、模式的应用场景 上面实例代码中的线程池其实便是实际工作会用到的场景之一,整体概括如下:
在应用场景中,某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如web中的配置对象、数据库的连接池等。
当某些类需要频繁地实例化,而创建的对象又频繁地被销毁的时候,如多线程的线程池,网络连接池等。
参考文章:单例模式(单例设计模式)详解 单例模式 - - 防止序列化破坏单例模式 单例模式的攻击之序列化与反序列化 设计模式之单例模式–扩展篇(多例模式)