更多最新文章欢迎大家访问我的个人博客😄:豆腐别馆
在某些系统中,为了节省内存资源,保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。
一、模式的定义
-
单例模式(Singleton),指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容不一致等错误。
-
在计算机系统中,还有Windows的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、web应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
-
单例模式有三个特点:
(1)单例类只有一个实例对象。
(2)该单例对象必须由单例类自行创建。
(3)单例类对外提供一个访问该单例的全局访问点。
二、模式的实现
单例模式通常有两种实现形式。
1. 懒汉式单例
(1)示例代码
以项目中常见的线程池为例,来创建一个懒汉式的单例线程池:
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;
/**
* 懒汉式单例线程池
*
* @author doufuplus
*/
public class LazySingleton {
// 使用volatile关键字禁止指令重排序,所有对该变量的读写都是直接操作共享内存
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)示例代码
依旧以线程池为例,来创建一个饿汉式的单例线程池:
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;
/**
* 饿汉式单例线程池
*
* @author doufuplus
*/
public class HungrySingleton {
// 注意使用static及final修饰变量
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
关键字,所以性能也有所保证。好了,话不多说,依旧以创建单例线程池为例,上代码:
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;
/**
* Holder单例线程池
*
* @author doufuplus
*/
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
,也不需要再去考虑如何写好内部类,更不用再担心万一关键字加错后会发生何种灾难性后果,你只需要在普通方法里写好自己的业务代码即可,堪称无脑万金油。 - 利用了枚举的特性来保证线程的安全。
- 利用了枚举的特性防止反射强行调用构造方法 。
- 依旧利用了枚举的特性,利用枚举提供的自动序列化机制,从而防止反序列化的时候会去创建新的对象。
- 其它模式都存在反射调用及反序列化破坏单例弊端。(下文会提到该问题及解决办法)
老规矩,创建单例线程池,上代码:
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;
/**
* 枚举式单例线程池
*
* @author doufuplus
*/
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)示例代码
首先先编写个反射用例,如下:
package com.yls.cloud.product.utils;
import java.lang.reflect.Constructor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 反射破坏单例测试
*
* @author doufuplus
*/
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)解决办法
如果要抵御这种攻击,就要防止构造函数被成功调用超过一次。因此可以在构造函数中对实例化次数进行统计,大于一次我们就抛出异常。因此我们需将原私有构造代码改造如下:
/**
* 调用次数统计
*/
private static int count = 0;
/**
* 私有化构造方法避免类在外部被实例化
* 注:加入计数器防止被多次调用(反射破坏)
*/
private LazySingleton() {
synchronized (LazySingleton.class) {
if (count > 0) {
throw new RuntimeException("被创建了超过一个实例!当前单例已被侵犯!");
}
count++;
}
}
改造好后,我们再运行上文的反射调用代码,就可以看到已经被成功拦截掉了:
2. 序列化破解单例
(1)示例代码
序列化及反序列化测试:
package com.yls.cloud.product.utils;
import java.io.*;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 反序列化破坏单例
*
* @author doufuplus
*/
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);
}
}
上文的代码由于是生成的线程池,导致跑测试代码时,序列化反序列化错误,为方便直接新写一个简单测试代码:
package com.yls.cloud.product.utils;
import java.io.Serializable;
/**
* 简单饿汉式单例
*
* @author doufuplus
*/
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()
方法即可,完整代码如下:
package com.yls.cloud.product.utils;
import java.io.Serializable;
/**
* 简单饿汉式单例
*
* @author doufuplus
*/
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 中,当需要时则可以随机获取。
这样的好处是我们可以决定内存中存在有多少个实例,可以修正单例模式带来的性能问题。示例代码如下:
package com.yls.cloud.product.utils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Random;
/**
* 单例模式扩展之有限多例模式
*
* @author doufuplus
*/
public class SingletonExtend implements Serializable {
/**
* 调用次数统计,防止反射破坏
*/
private static int countCall = 0;
/**
* 指定内存中可以存在的实例数
*/
private static int maxObj = 3;
/**
* 该容器用来装SingletonExtend实例
*/
private static ArrayList<SingletonExtend> instances = new ArrayList<SingletonExtend>();
/**
* 该容器用来装SingletonExtend的属性name
*/
private static ArrayList<String> names = new ArrayList<String>();
/**
* 生成一个0,1之间的随机数
*/
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;
}
/**
* 将SingletonExtend实例装入容器中
*/
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);
//随机取出集合容器中的一个SingletonExtend实例
return instances.get(number);
}
/**
* 给SingletonExtend类添加一个动作
*/
public void hello() {
System.out.println("Hello:" + names.get(number));
}
public static void main(String[] args) {
// 模拟5个客户端随机取出两个SingletonExtend对象中的一个
for (int i = 0; i < 5; i++) {
SingletonExtend instance = SingletonExtend.getInstance();
instance.hello();
}
}
}
运行结果如下:
五、模式的应用场景
上面实例代码中的线程池其实便是实际工作会用到的场景之一,整体概括如下:
- 在应用场景中,某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如web中的配置对象、数据库的连接池等。
- 当某些类需要频繁地实例化,而创建的对象又频繁地被销毁的时候,如多线程的线程池,网络连接池等。
参考文章:
单例模式(单例设计模式)详解
单例模式 - - 防止序列化破坏单例模式
单例模式的攻击之序列化与反序列化
设计模式之单例模式–扩展篇(多例模式)