GOF23设计模式之:单例模式
-
核心作用
保证一个类只有一个实例,并且提供一个访问该实例的全局访问点
-
常见应用场景
-
Windows的Task Manager(任务管理器)就是很典型的单例模式
-
Windows的Recycle Bin(回收站)也是典型的单例应用.在整个系统运行过程中,回收站一直维护着仅有的一个实例
-
项目中,读取配置文件的类,一般也只有一个对象.没有必要每次使用配置文件数据都new一个对象去读取
-
网站的计数器,一般也是采用单例模式实现,否则难以同步
-
应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加
-
数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源.
-
操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统
-
Application也是单例的典犁府用(Servlet中会涉及到)
-
在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理
-
在Servlet编程中,每个Servlet也是单例
-
在spring MVC架构/struts1框架中,控制器对象也是单例
-
-
单例模式的优点
- 由于单例模式只生产一个实例,减少了系统性能开销,当一个对象的生产需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存来解决
-
单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例,负责所有数据表的映射处理
-
常见的五种单例模式实现方式
-
主要
-
饿汉式(线程安全,调用效率高,但是不能延时加载)
-
懒汉式(线程安全,调用效率不高,但是可以延时加载)
-
-
其他
-
双重检测锁式(由于JVM底层内部模型原因,偶尔会出现问题,不建议使用)
-
静态内部类式(线程安全,调用效率高,可以延时加载)
-
枚举单例(线程安全,调用效率高,不能延时加载)
-
-
如何选用
-
单例对象 占用资源少 不需要延时加载
枚举式 好于 饿汉式
-
单例对象 占用资源多 需要延时加载
静态内部类 好于 懒汉式
-
饿汉式实现(单例对象立即加载)
/**
* 饿汉式
* @author Matrix42
*
*/
public class SingletonDemo01 {
//类初始化时立即加载
private static SingletonDemo01 instance = new SingletonDemo01();
private SingletonDemo01(){
}
//方法没有同步,调用效率高
public static SingletonDemo01 getInstance(){
return instance;
}
}
-
饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题.虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题.因此,可以省略synchronized关键字
-
问题:如果只加载本类,而不调用getInstance(),甚至永远没有调用,则会造成资源浪费!
懒汉式实现(单例对象延迟加载)
public class LazayLoad {
private static LazayLoad instance;
private LazayLoad(){
}
public static synchronized LazayLoad getInstance(){
if (instance==null) {
instance = new LazayLoad();
}
return instance;
}
}
- 问题:资源利用率高了,但是每次调用getInstance()方法都要同步,并发效率低了
双重检测锁实现
-
这个模式将同步内容放到if内部,提高了执行的效率,不比每次获取对象时都进行同步,只有第一次才同步,创建了以后就没必要了
-
由于JVM底层内部模型原因,偶尔会出现问题,不建议使用
public class SingletonDemo3 {
private static SingletonDemo3 instance = null;
public static SingletonDemo3 getInstance() {
if (instance == null) {
SingletonDemo3 sc;
synchronized (SingletonDemo3.class) {
sc = instance;
if (sc == null) {
synchronized (SingletonDemo3.class) {
if(sc == null) {
sc = new SingletonDemo3();
}
}
instance = sc;
}
}
}
return instance;
}
private SingletonDemo3() {
}
}
静态内部类实现方式(也是一种懒加载方式)
-
外部类没有static属性,不会像饿汉式那样立即加载对象
-
只有真正调用getInstance(),才会加载静态内部类.加载时是线程安全的.instance是static final类型,保证了内存中只有一个实例存在,而且只能被赋值一次,从而保证了线程安全性
- 兼备了并发高效调用和延迟加载的优势
public class SingletonDemo04 {
private static class SingketonClassInstance{
private static final SingletonDemo04 instance = new SingletonDemo04();
}
public static SingletonDemo04 getInstance(){
return SingketonClassInstance.instance;
}
private SingletonDemo04(){
}
}
使用枚举实现单例模式
-
优点
-
实现简单
-
枚举本身就是单例模式.由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
-
-
缺点
- 无延迟加载
public enum SingletonDemo05 {
//这个枚举元素本身就是单例对象
INSTANCE;
//添加自己需要的操作
public void singtonOperation(){
}
}
问题
-
反射可以破解上面几种(不包含枚举)实现方式!(可以在构造方法中手动抛出异常来控制)
-
反序列化也可以破解上面几种(不包含枚举)实现方式,可以通过定义readResolve()防止获得不同对象
- 反序列化时,如果对象所在类定义了readResolve()(实际是一种回调)定义返回哪个对象
import java.io.Serializable;
/**
* 测试懒汉式单例模式(如何防止反射和反序列化漏洞)
* @author Matrix42
*
*/
public class Demo06 implements Serializable{
private static final Demo06 instance = new Demo06();
public static Demo06 getInstance(){
return instance;
}
//更严密,一般不用考虑这么多
private Demo06(){
if(instance!=null){
throw new RuntimeException();
}
}
//反序列化时,如果定义了readResolve()则直接返回此方法指定的对象.而不需要创建新对象
private Object readResolve(){
return instance;
}
}
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationTargetException;
public class Client {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException {
Demo06 s1 = Demo06.getInstance();
Demo06 s2 = Demo06.getInstance();
System.out.println(s1);
System.out.println(s2);
//通过反射方式调用私有构造器
// Class<Demo06> clazz = (Class<Demo06>) Class.forName("design.sington.Demo06");
//
// Constructor<Demo06> constructor = clazz.getDeclaredConstructor(null);
//
// constructor.setAccessible(true);
//
// Demo06 s3 = constructor.newInstance();
//
// System.out.println(s3);
//通过反序列化破解
FileOutputStream fos = new FileOutputStream("d:/a.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1);
oos.close();
fos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:/a.txt")));
Demo06 s3 = (Demo06) ois.readObject();
System.out.println(s3);
}
}
常见的五种单例模式在多线程环境下的效率测试
-
只关注相对值即可,在不同环境下不同的程序测得值会不一样
-
CountDownLatch
-
同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待
-
countDown()当前线程调用此方法,则计数减一(建议放在finally里执行)
-
await()调用此方法会一直阻塞当前线程,直到计数器的值为0
-
-
import java.util.concurrent.CountDownLatch;
/**
* 测试五种创建单例模式的效率
* @author Matrix42
*
*/
public class Client02 {
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
int threadNum = 10;
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
for(int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<1000000;i++){
Object object = SingletonDemo01.getInstance();
}
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("总耗时: "+(end-start));
}
}