Java设计模式及应用场景之《单例模式》

一、单例模式定义

保证一个类只能有一个实例,并提供一个访问这个唯一实例的全局访问点。

二、单例模式的结构和说明

在这里插入图片描述
Singleton:负责创建Singleton自己的唯一实例,并提供一个getInstance方法,供外部来访问这个唯一实例。

三、懒汉式和饿汉式的实现

单例模式有两种典型的创建方式,一种叫懒汉式,另一种叫饿汉式。

1、懒汉式

  懒汉式的特点是延迟加载,你不用我就不创建,等到第一次调用的时候,才去创建实例对象。

public class Singleton {
	//4:定义一个变量来存储创建好的类实例
	//5:因为这个变量要在静态方法中使用,所以需要加上static修饰
	private static Singleton instance = null;
	//1:私有化构造方法,在内部控制创建实例的数目,防止在外部创建实例
	private Singleton(){
	}
	//2:定义一个方法来为客户端提供类实例
	//3:这个方法需要定义成类方法,也就是要加static
	public static  Singleton getInstance(){
		//6:判断存储实例的变量是否有值
		if(instance == null){
			//6.1:如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
			instance = new Singleton();
		}
		//6.2:如果有值,那就直接使用
		return instance;
	}
}
2、饿汉式

  饿汉式的特点是饥不择食,在加载类的时候就会创建类的实例。

public class Singleton {
	//4:定义一个静态变量来存储创建好的类实例
	//直接在这里创建类实例,只会在类加载的时候创建一次
	private static Singleton instance = new Singleton();
	//1:私有化构造方法,好在内部控制创建实例的数目
	private Singleton(){
	}
	//2:定义一个方法来为客户端提供类实例
	//3:这个方法需要定义成类方法,也就是要加static
	public static Singleton getInstance(){
		//5:直接使用已经创建好的实例
		return instance;
	}
}
四、懒汉式和饿汉式的优缺点

1、时间和空间方面

  • 懒汉式是典型的时间换空间**,每次获取实例时都会去判断是否需要创建实例,浪费判断时间。而如果一直没有人使用的话,就不会去创建实例,节约内存空间。
  • 饿汉式是典型的空间换时间**,当类加载时就会创建实例,不管用不用,先创建出来,再以后调用时,就不需要去判断了,节省了运行时间。

2、线程安全方面

  • 不加同步的懒汉式是线程不安全的,可能会出现并发问题。
  • 饿汉式是线程安全的,因为虚拟机保证只会加载一次,在加载类的时候不会发生并发问题。
五、双重检查加锁方式的实现

  为了解决懒汉式的线程安全问题,我们可以在获取实例方法上加上synchronized,如下:
  public static synchronized Singleton getInstance(){...}
  但是这样会降低整个访问的速度。那么,怎么才能既实现线程安全,又能让性能不受到很大的影响呢?我们可以利用 “双重检查加锁” 的方式来实现。

public class Singleton {
	/**
	 * 对保存实例的变量添加volatile的修饰
	 */
	private volatile static Singleton instance = null;
	private Singleton(){
	}
	public static  Singleton getInstance(){
		//先检查实例是否存在,如果不存在才进入下面的同步块
		if(instance == null){
			//同步块,线程安全的创建实例
			synchronized(Singleton.class){
				//再次检查实例是否存在,如果不存在才真的创建实例
				if(instance == null){
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

“双重检查加锁” 的方式会用到关键字volatile,这里volatile的作用是防止在创建单例对象时JVM的 指令重排序 。 JVM 为了提高程序的运行效率,会对代码按照 JVM 编译器认为最优的顺序执行,从而可能打乱代码的执行顺序,这也就是我们所说的 指令重排序

我们先来看看我们期望的构建对象的操作指令:

  • 指令1:分配一块内存 M;
  • 指令2:在内存 M 上初始化 Singleton 对象;
  • 指令3:然后将 M 的地址赋值给 instance 变量。

如果不加volatile,JVM 编译器上可能不是这样,可能会被优化成如下指令:

  • 指令1:分配一块内存 M;
  • 指令2:将 M 的地址赋值给 instance 变量;
  • 指令3:在内存 M 上初始化 Singleton 对象。

这个指令重排的优化,就可能会导致线程安全问题。假设线程1刚执行完指令2,此时instance已经不是null了,但是还没有执行指令3对instance对象进行初始化。这时候又来一个线程2,线程2看到instance不是null,直接返回instance,并调用instance的方法或者成员变量,这时将可能触发空指针异常。

六、类级内部类方式的实现

  前面几种方式,都存在小小的缺陷。那么有没有什么方式,既能实现延迟加载,又能实现线程安全呢?类级内部类的方式,就同时实现了延迟加载和线程安全。

public class Singleton {
	/**
	 * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,
	 * 而且只有被调用到才会装载,从而实现了延迟加载
	 */
	private static class SingletonHolder{
		/**
		 * 静态初始化器,由JVM来保证线程安全
		 */
		private static Singleton instance = new Singleton();
	}
	/**
	 * 私有化构造方法
	 */
	private Singleton(){
	}
	
	public static  Singleton getInstance(){
		return SingletonHolder.instance;
	}
}

  当getInstance()方法第一次被调用的时候,SingletonHolder类得到初始化,当SingletonHolder类被加载并初始化时,会初始化它的静态域,从而创建Singleton的实例。由于是静态的域,因此只会在虚拟机加载类的时候初始化一次,并由虚拟机来保证它的线程安全。

七、枚举方式的实现 (最佳方式)

 前面几种方式都有共同的缺点,从而导致多实例的出现。

  • 每次反序列化一个序列化的对象时都会创建一个新的实例。
  • 可以使用反射强行调用私有构造器。

 而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。

单元素的枚举类型已经成为实现单例的最佳方式。

/**
 * 使用枚举来实现单例模式的示例
 */
public enum Singleton {	
	/**
	 * 定义一个枚举的元素,它就代表了Singleton的一个实例
	 */
	INSTANCE;
	
	/**
	 * 示意方法,单例可以有自己的操作
	 */
	public void singletonOperation(){
		//功能处理
	}
}

  使用枚举的方式来实现单例会更加简洁,而且无偿的提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。

八、单例模式的应用场景
  • 网站的计数器,一般是采用单例模式实现,否则难以同步。
  • 应用程序的日志应用,一般都采用单例模式实现,这是由于共享的日志文件一直处于打开状态,只能有一个实例去操作,否则内容不好追加。
  • Web应用的配置文件的读取,一般也采用单例模式,这个是由于配置文件是共享的资源。
  • 数据库连接池的设计一般也是采用单例模式。
  • 多线程的线程池的设计一般也是采用单例模式。
  • 在Spring中创建的Bean实例默认都是单例模式存在的。
  • 在Spring MVC框架中,每个控制器对象也是单例。
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晓呆同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值