【设计模式】深入理解单例模式

单例模式

一 概述

单例模式是一个本身是一个非常容易理解的模式,但是由于本身的一些缺陷所以有了许多对单例模式的改进,因此也比较复杂。这篇文章是我对单例模式学习时的一个总结,希望对大家理解单例模式有所帮助

什么是单例模式
  • 概念:保证一个类仅有一个实例,并提供一个访问它的全局访问点
  • 概念简单,理解起来也不难,实现这样的方式就是将构造函数私有化,将自身的引用声明成一个全局变量,再提供一个初始化变量的方法。但是也有许多问题,下面我将会在实现方式中慢慢讨论。
单例模式的实现种类

单例模式有许多实现方法,来应对各种问题,其中就有:1.懒汉模式,2.饿汉模式,3.静态内部类,4.枚举类型。下面我们会对每种类型详细讨论并且优化。

二 每种实现方式与优化

实现方式

1)懒汉模式
  • 懒汉模式,字面意思就能充分体现出这种实现方式的特点:只有在使用的时候才进行初始化,延迟加载。
    具体实现:

public class Lazy {
	//申明一个对自身的引用
	private static Lazy lazy;
	//私有的构造函数
	private Lazy(){
		
	}
	//对自身引用的初始化
	public static Lazy getLazy(){
		
		if(lazy==null){
			lazy = new Lazy();
		}
		
		return lazy;
	}
}

这就是一个最简单的懒汉模式。

  • 但是,这样真的能保证只创建一个对象吗?
    其实在单线程的情况下是只创建一个对象,但是,多线程就不一定了看代码:
    1.主函数
public class MainClass {
	public static void main(String[] args) {
		//线程一
		new Thread(new Runnable(){
			@Override
			public void run() {
				Lazy l = Lazy.getLazy();
				System.out.println(l);
			}}).start();
		//线程二
		new Thread(new Runnable(){
			@Override
			public void run() {
				Lazy l = Lazy.getLazy();
				System.out.println(l);
			}}).start();
	}
		
}
public class Lazy {
	//申明一个对自身的引用
	private static Lazy lazy;
	//私有的构造函数
	private Lazy(){
		
	}
	//对自身引用的初始化
	public static Lazy getLazy(){
		
		if(lazy==null){
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			lazy = new Lazy();
		
		}
		
		return lazy;
	}
}

在这里插入图片描述
通过程序我们模拟了多线程的访问,发现出现了两个对象。
这就是懒汉模式的一个问题:线程不安全
所以就有了这样的解决方法

public class Lazy {
	//申明一个对自身的引用
	private static Lazy lazy;
	//私有的构造函数
	private Lazy(){
		
	}
	//对自身引用的初始化
	public static Lazy getLazy(){
		
		if(lazy==null){
			//加同步代码块进行控制。
			synchronized (Lazy.class) {
				if(lazy==null){
					lazy = new Lazy();
				}
			}
		}
		return lazy;
	}
}

结果
在这里插入图片描述
我们发现这样的问题解决了。需要注意的是这里synchronized关键没有加在方法上,因为这个方法是静态方法,如果加上synchronized相当于加在类上,会造成不小的开销

  • 但是这样真的就没有问题了吗?
    其实代码中的lazy = new Lazy();在JVM中会有三步:
    1)分配空间,返回一个指向该空间的内存引用,
    2)把内存空间进行初始化
    3)把内存引用赋值给lazy变量
    在多线程的情况下,2与3会出现重排序,即2与3发生调换。这样就有了问题:假设,线程一进入getLazy()方法,执行lazy = new Lazy(),线程二到来卡在静态代码块处等待,此时线程三到来,由于多线程的原因,线程一在执行lazy = new Lazy()时发生重排序,先执行了3)把内存引用赋值给了lazy变量,还没有执行2)初始化,但是对于线程三来说判断条件lazy==null为false,所以他继续向下执行,直接返回一个空的lazy,此时代码就出现了空指针异常。(这个没有想出演示方法)
    解决这个问题的方法就是在声明自身引用时加上volatile,这样volatile修饰的lazy指向的内存空间中的指令集就不会发生重排序。
  • 这样就有了比较严谨和完整的懒汉模式的代码
public class Lazy {
	//申明一个对自身的引用
	private volatile static Lazy lazy;
	//私有的构造函数
	private Lazy(){
		
	}
	//对自身引用的初始化
	public static Lazy getLazy(){
		
		if(lazy==null){
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			//加同步锁进行控制。
			synchronized (Lazy.class) {
				if(lazy==null){
					lazy = new Lazy();
				}
			}
		
		}
		
		return lazy;
	}
}
  • 其实在spring框架中就有许多这样结构的代码
    在这里插入图片描述
2)饿汉模式

饿汉模式:在类加载阶段,完成了实例的初始化,也是很符合名字。
直接看代码:

public class Hungry {
	//在类加载的时候就对类进行了初始化
	private static Hungry hungry = new Hungry();
	
	private Hungry(){
	}
	
	public static Hungry getHungry(){
		return hungry;
	}
}

这里饿汉模式没有懒汉模式那么多问题,因为它通过类加载机制来保证了线程安全。

3)静态内部类与反射攻击

第三种实现方式是静态内部类,我认为这时饿汉模式与懒汉模式的结合。
看代码

public class HuAndla {
	static class HL{
		private static HuAndla hl = new HuAndla();
	}
	
	private HuAndla(){}
	
	public static HuAndla getHuAndla(){
		return HL.hl;
	}
}

静态内部类就是在类中的静态内部类中进行类的初始化。按照整体来说他是懒汉模式,按照他的初始化方式来说他又是饿汉模式。
但是这样的方式也有缺点,就是通过反射进行创建对象的时候就会出现问题
看代码

public class MainTest {
	public static void main(String[] args) throws NoSuchMethodException, SecurityException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		//通过反射获取HuAndla的构造函数
		Constructor<HuAndla> c = HuAndla.class.getDeclaredConstructor();
		//设置对构造函数的访问权限
		c.setAccessible(true);
		//对构造函数进行调用,实例化
		HuAndla hl1 = c.newInstance();
		
		//通过普通的模式进行对象的获取
		HuAndla hl2 = HuAndla.getHuAndla();
		System.out.println(hl1==hl2);
	}
}

结果
在这里插入图片描述
这里我们发现通过反射创建出来的对象对原有的结构进行了破坏,创建了两个不同的对象。其实不只是静态内部类有这样的问题,就连上面的懒汉模式与饿汉模式也有这样的问题
这里,有两种解决方式

  • 第一种就是在构造函数中进行异常的抛出。
public class HuAndla {
	static class HL{
		private static HuAndla hl = new HuAndla();
	}
	
	private HuAndla(){
		//如果hl已经实例化了就没必要在实例化了,直接抛出异常
		if(HL.hl!=null){
			throw new RuntimeException("已经实例化过了");
		}
	}
	
	public static HuAndla getHuAndla(){
		return HL.hl;
	}
}

这样如果通过反射进行对象的创建就会报异常。

  • 第二种解决方法就是用枚举类型来进行单例模式的创建,下面会进行具体说明。
4)枚举类型

直接看代码

public class Enum {
	
	private Enum(){
		
	}
	//构建枚举类的内部类
	private enum E{
		INSTANCE;
		
		private final Enum instance;
		
		E(){
			instance = new Enum();
		}
		
		private Enum getEnum(){
			return instance;
		}
	}
	
	public static Enum getEnum(){
		return E.INSTANCE.getEnum();
	}
	
}

这样就是枚举类型的单例模式,为什么枚举类型的内部类会防止反射进行不同对象的创建呢?
从反射的源码中我们就可以看到原因
在这里插入图片描述
从源码中我们可以看到如果是枚举类型就会抛出异常。
枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。但是失去了类的一些特性,没有延迟加载,用的人也很少。

三 模式的优点与适用场景

优点

单例模式中单例类的唯一实例由单例类本省控制,所以可以很好的控制用户何时访问它。

场景

当系统需要某个类只能有一个实例,比如系统中的线程池,数据库连接池等。

这就是我所理解的单例模式,如果有错误或者你有更好的想法请告诉我。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值