设计模式(六)—— 单例模式(定义、案例分析、特点、缺点)


前言

文章内容主要参考了刘伟主编的《设计模式(第2版)》,同时也结合了自己的一些思考和理解,希望能帮到大家。


本篇的单例模式可以说是我们使用率非常高也非常常见的设计模式!

正文

一、定义

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。是一个对象创建模式。
在这个模式中,我们要特别关注怎么实现单例。这个就是最大的难点,而目前单例模式其实有非常多的实现方式,下面我们先讲述一个小案例理解,之后拓展饿汉式和懒汉式。

二、情景假设

在操作系统中,打印池(Print Spooler)是一个用于管理打印任务的应用程序,通过打印池用户可以删除、中止或者改变打印任务的优先级,在一个系统中只允许运行一个打印池对象,如果重复创建打印池则抛出异常。现使用单例模式来模拟实现打印池的设计。

类图比较简单,主要关注属性和方法该怎么编写,下面看分析

三、情景分析

关于上面情景的类图(具体分析在下面)
在这里插入图片描述
首先我们要知道我们在new一个对象,先调用的是什么方法?对了!构造方法,所以每new一次就会创建一下,那么我们既然想要控制不会随便的构建,我们能想到第一步就是让构造函数私有化,再者问题来了,构造函数都私有化了,我怎么new??所以原来构造函数走不通了,那么我们就需要通过另外的入口去创建,有人可能会问我对象都创建不出来我怎么调用里面另外的借口去创建,太矛盾了这!确实是个问题,但不知道大家想起了类方法吗?没错我们可以设定为static,通过类去调用类方法去创建对象,然后还没完!我们还要定义一个变量去存第一次创建的这个实例,因为后续如果又想创建对象,这时候我们可以通过这个变量就知道是不是创建好了,如果创建好了就返回这个实例,否则我们就新建,注意这个私有变量是static的,因为static方法只能使用static属性。

//单例类PrintSpoolerSingleton
public class PrintSpoolerSingleton
{
	//静态属性存储实例
	public static PrintSpoolerSingleton instance = null;
	//私有化构造函数,避免对外的重复创建
	private PrintSpoolerSingleton(){}
	//自建对外创建借口,并设定为类方法
	public staticPrintSpoolerSingleton getInstance() throws PrintSpoolerException{
		if(instance == null){
			System.out.println("创建打印池");
			instance = new PrintSpoolerSingeleton();
		} else {
			throw new PrintSpoolerExcption("打印池正在工作中!");
		}
		return instance;
	}

	//打印池的方法
	public void manageJobs(){
		System.out.println("管理打印任务");
	}
}

//下面是自定义的打印池Exception类
public class PrintSpoolerException extends Exception{
	public PrintSpoolerException(String message){
		super(message);
	}
}

接下来是客户端的代码:

public class Client
{
	public static void main(String args[])
	{
		 PrintSpoolerSingleton ps1,ps2;
         try
         {
         	ps1 = new PrintSpoolerSingleton.getInstance();
         	ps1.manageJobs();
         }
         catch(PrintSpoolerException e)
         {
         	System.out.println(e.getMessage());
         }


		 try
         {
         	ps2 = new PrintSpoolerSingleton.getInstance();
         	ps2.manageJobs();
         }
         catch(PrintSpoolerException e)
         {
         	//输出台应该会产生异常,因为重复创建了!!!
         	System.out.println(e.getMessage());
         }
	}
}

好的学到这是不是觉得还OK,也不是很难,但是这是有问题滴!
接下来看分析及拓展吧!!

四、模式结构及分析

(一) 五种方式

目前所看到的所有文章和我学习到的,主要是五种方式编写单例模式。

  1. 饿汉式
  2. 懒汉式
  3. 双重检查锁定
  4. 静态内部类方式
  5. 枚举方式

个人觉得其实整体就是分为两类,一类是懒汉,一类是饿汉。但因为其他三种方式经常被单独拎出来讲解所以经常被说成五种方式。懒汉式和饿汉式其实是相对的,懒意味着具有拖延症,不到时候不会主动创建,饿汉式就是饿的不行立刻去获取创建。
我们都知道要想创建对象,肯定先要加载类的相关信息,所以现有类加载才会有创建对象,懒汉式就是在new对象的时候才创建那个”单例“,饿汉式就是类加载就已经创建了。

  • 懒汉式
  1. 懒汉式第一版:在多线程的情况下是有问题的,多线程情况下是可能同时访问到intance都是null所以创建多个,违背了单例模式。
	public LazySingleton{
		private static LazySingleton instance = null;
		private LazySingleton(){}
		public static LazySingleton getInstance(){
			if(instance == null){
				return new LazySingleton();
			}
			return instance;
		}
	}
  1. 懒汉式第二版:既然多线程不行,就加锁!把所加在方法上!但其实如果在高并发下,其实获取锁释放锁的成本太高了,所以多线程一般不写这种方式
	public LazySingleton{
		private static LazySingleton instance = null;
		private LazySingleton(){}
		synchronized public static LazySingleton getInstance(){
			if(instance == null){
				return new LazySingleton();
			}
			return instance;
		}
	}
  • 双重检查锁机制(针对懒汉式第二版)
	public DoubleCheck{
		//volatile关键字是为了防止指令重排保证有序性
		private volatile static DoubleCheck instance = null;
		private DoubleCheck(){}
		public static DoubleCheck getInstance(){
			if(instance == null){
				synchronized(DoubleCheck.class){
					if(instance == null){
						instance = new DoubleCheck();
					}
				}
			}
			return instance;
		}
	}

知道大家看晕了,在这里我们先看getInstance方法,双重检查其实指的就是这里两次判断instance == null。刚说到饿汉式的锁太暴力了直接加在了方法上,后面如果早就已经创建好了单例,其实instance早就不是null,我们加锁是考虑第一次创建实例时,第一次并发创建会重复,所以我们只要考虑第一次的情况,而不是直接加在方法上不考虑第一次还是第二次,而每一次都要加锁,明显没有必要。所以我们会想到把锁加在了代码块上放在函数内部,开头先用if拦截后面就不会用到锁:

public static DoubleCheck getInstance(){
		if(instance == null) {
			synchronized(DoubleCheck.class){
				instance = new DoubleCheck();
			}
		}
		return instance;
}

看起来好像没问题,但实际上上面代码已经很接近我们的答案了,但是为什么双重检查又在里面加了判断了呢?因为实际上第一次多个进程请求获取实例,第一层的if判断大家都是null,于是进入到锁,只有一个进程获取锁,而其他线程呢?他们退出了嘛?并没有,他们会等待抢着锁然后进去创建对象!!所以本质就是还是创建了多个对象根本就不是单例!所以锁内加上if检查是让排队等锁的那几位进程进去锁后也创建不了,这就是双重检查。
最后就是volatile关键字,这个是用来防止指令重排,创建对象的过程其实可以看成是几个指令的过程,其中指令执行为了加快执行,会进行优化排序,这个时候可能会出现先返回对象再分配内存,那返回后的对象直接使用就有可能存在空对象的情况,就有可能出错,所以就必须加上这个关键字。

  • 饿汉式(类加载会加载静态变量,所以直接定义变量时就new了,很好理解,不足就是一开始就创建了,而不理我们使不使用,所以浪费一定的内存)
	public EagerSingleton{
		private static final EagerSingleton instance = new EagerSingleton();//静态变量处就已经创建类
		private EagerSingleton(){}
		public static EagerSingleton getInstance(){
			return instance;
		}
	}
  • 静态内部类(经过我资料的查阅,我一开始以为是饿汉式,因为我们是写了一个静态的内部类,还是静态的,但是经网友验证发现内部类里面的静态变量并不是单例类类加载就创建的,所以我把这个归为懒汉式的)
	public Singleton{
		//静态变量换成了静态的内部类,在返回实例时,就会加载内部类
		//也就自然的在类加载时创建一次,也就是单例了
		private static static class SingletonHolder{
			private static final Singleton INSTANCE = new Singleton();
		}
		private Singleton(){}
		public static Singleton getInstance(){
			return SingletonHolder.INSTANCE;
		}
	}
  • 枚举方式
public enum Singleton{
	INSTANCE;
	public void doAnything(){
		...
	}
}
//调用方式
public class Main {
    public static void main(String[] args) {
        Singleton.INSTANCE.doAnything();
    }

}

(二) 五种方式的总结

  • 归类:饿汉:饿汉式;懒汉:懒汉式、双重检查锁、内部静态类;未知分类:枚举方式无法考究查阅了一下有人说两个都不是,这可能需要大佬们来验证啦!
  • 饿汉式不支持多线程,其他都支持多线程
  • 枚举方式可以防止序列化攻击和防止反射攻击,其他都不行。可以看看这篇:枚举方式的单例模式

(三) 模式分析

(1) 模式特点
  1. 提供了对唯一实例的受控访问
  2. 可以节约系统资源,提高系统的性能
  3. 允许可变数目的实例(多例类,在这里,其实我们可以考虑使用与单例控制相似的方法来获得指定个数的对象实例)
  4. 饿汉和懒汉比较:
    • 饿汉式单例类:无须考虑多个线程同时访问的问题;调用速度和反应时间优于懒汉式单例;资源利用效率不及懒汉式单例;系统加载时间可能会比较长
    • 懒汉式单例类:实现了延迟加载;必须处理好多个线程同时访问的问题;需通过双重检查锁定等机制进行控制,将导致系统性能受到一定影响
(2) 模式缺点
  1. 扩展困难(缺少抽象层)
  2. 单例类的职责过重(充当了工厂角色、提供工厂方法、有充当产品角色等等)
  3. 由于自动垃圾回收机制,可能会导致共享的单例对象的状态丢失(长期未使用给回收,重新创建原先的状态丢失)

五、使用情景

  • 系统只需要一个实例对象,或者因为资源消耗太大而只允许创建一个对象
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例

六、延申及拓展

  • “开闭原则”的倾斜性(特定情况是好的)
    对于设计多个产品族和产品等级结构的系统,其功能增强包括了两个方面:
    • 增加产品族,该模式很好的支持了“开闭原则”,只需要加一个具体实现工厂类即可。
    • 增加新的产品等级结构,需要修改抽象工厂类和所有的工厂角色,违背“开闭原则”
  • 工厂模式的退化
    • 当每一个具体工厂类只有一种产品对象,其实就退化成了工厂方法模式。工厂方法模式种的抽象工厂和具体实现类合并且设置为静态方法创建,这又退化成了简单工厂模式

总结

本篇文章主要介绍设计模式的单例模式,该模式是最常见的,也是非常需要掌握的,其实无论哪几种方式记住单例思想。1.静态属性存储单例;2.构造方法私有化;3.对外创建单例接口;

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值