设计模式(四)创建型模式 - 单例模式

更多最新文章欢迎大家访问我的个人博客😄:豆腐别馆

在某些系统中,为了节省内存资源,保证数据内容的一致性,对某些类要求只能创建一个实例,这就是所谓的单例模式。

一、模式的定义

  • 单例模式(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()方法时才会去创建这个实例。顾名思义通俗点讲就是比较懒,要用到了我再创建,没用到我就不创建。

  • 缺点
    ① 这就是典型的时间换空间,也就是每次获取实例都会进行判断,看看是否需要创建实例,这样显然就浪费了每次判断的时间。
    ② 同时我们可以看到为了保证多线程下的线程安全问题,我们使用了volatilesynchronized关键字,这样线程安全问题确实可以得到保障,但是每次访问时都需要同步,这样就会影响性能,且会消耗更多的资源。

  • 优点
    当然,时间换空间也有好处,如果一直没有人使用的话,那就不会创建实例,则可以节约内存空间。这也就是懒汉式单例的优点。

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. 枚举式单例

终于到了最后一种模式了,是的,此种模式正是被大家推崇为最优实现单例模式的方式 - - 枚举式单例模式。为什么说是大家都推崇的呢?我罗列下此种方式的好处,相信你也会推崇它:

  • 首先当然是写法简单,因为自从有了它,你将不需要再去考虑我要怎样添加各种关键字如volatilestaticfinal,也不需要再去考虑如何写好内部类,更不用再担心万一关键字加错后会发生何种灾难性后果,你只需要在普通方法里写好自己的业务代码即可,堪称无脑万金油。
  • 利用了枚举的特性来保证线程的安全。
  • 利用了枚举的特性防止反射强行调用构造方法 。
  • 依旧利用了枚举的特性,利用枚举提供的自动序列化机制,从而防止反序列化的时候会去创建新的对象。
  • 其它模式都存在反射调用反序列化破坏单例弊端。(下文会提到该问题及解决办法)

老规矩,创建单例线程池,上代码:

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中的配置对象、数据库的连接池等。
  • 当某些类需要频繁地实例化,而创建的对象又频繁地被销毁的时候,如多线程的线程池,网络连接池等。

参考文章:
单例模式(单例设计模式)详解
单例模式 - - 防止序列化破坏单例模式
单例模式的攻击之序列化与反序列化
设计模式之单例模式–扩展篇(多例模式)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值