Singleton Pattern 单例模式,作为创建型模式的一种,其保证了类的实例对象只有一个,并对外提供此唯一实例的访问接口。
23中设计模式 |
---|
概述
对于单例模式而言,其最核心的目的就是为了保证该类的实例对象是唯一的。为此一方面,需要将该类的构造函数设为private,另一方面,该类需要在内部完成实例的构造并对外提供访问接口。单例模式的好处显而易见,可以避免频繁创建、销毁实例所带来的性能开销;但其缺点也同样明显,此类不仅需要描述业务逻辑,同时还需要构造出该类的唯一对象并对外提供访问接口,其显然违背了单一职责原则
实现
单例模式的思想虽然简单易懂,但实现起来却可谓是花样繁多、妙不可言。这里来介绍几种常见的单例模式的实现
饿汉式
如下实现最为简单,当 SingletonDemo1 类被加载到JVM中,即会完成实例化。即不是所谓的Lazy Load 延迟加载,故通常被称之为 “饿汉式” 单例。饿汉式类加载到内存后,就实例化一个单例,JVM保证线程安全。
其最大的问题就在,可能构造出来的实例对象从头到尾没有被使用过(没有调用过getInstance方法),从而浪费内存。可能有人会对此有些困惑,MessageResource01 类被加载到JVM中了,那肯定是因为调用了getInstance方法啊。难道还有别的原因?肯定有!如果SingletonDemo1类中还有其他静态方法,一旦被调用就会导致MessageResource01类被加载、初始化,此时即完成了实例的构造。
/**
* 饿汉式
* 类加载到内存后,就实例化一个单例,JVM保证线程安全
* 简单实用,推荐使用!
* 唯一缺点:不管用到与否,类装载时就完成实例化 比如 Class.forName("")
* (话说你不用的,你装载它干啥)
*/
public class MessageResource01 {
private static final MessageResource01 INSTANCE = new MessageResource01("我是饿汉式单例模式!");
private String description;
/**
* 私有构造器
* @param message
*/
private MessageResource01(String message){
this.description = message;
}
public void getInfo() {
System.out.println(description);
}
/**
* 提供实例的访问接口
* @return
*/
public static MessageResource01 getInstance() {
return INSTANCE;
}
public static void main(String[] args) {
MessageResource01 messageResource = MessageResource01.getInstance();
messageResource.getInfo();
}
}
测试结果 |
---|
懒汉式
前面说到,饿汉式单例会导致内存空间的浪费,那么有没有办法解决这个问题呢?答案是有的,这就是"懒汉式"单例。顾名思义,其实例不是在类加载、初始化时被构建的,而是在真正需要的时候才去创建,如下所示
/**
* 懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
*/
public class MessageResource02 {
private static MessageResource02 INSTANCE ;
private String description;
/**
* 私有构造器
*
* @param message
*/
private MessageResource02(String message) {
this.description = message;
}
public void getInfo() {
System.out.println(description);
}
/**
* 提供实例的访问接口
*
* @return
*/
public static MessageResource02 getInstance() {
if(INSTANCE == null){
INSTANCE = new MessageResource02("我是线程不安全的懒汉式单例模式!");
}
return INSTANCE;
}
public static void main(String[] args) {
MessageResource02 messageResource = MessageResource02.getInstance();
messageResource.getInfo();
}
测试结果 |
---|
"懒汉式"单例虽然实现了Lazy Load延迟加载,但是其存在一个很严重的问题,不是线程安全的。所以如果在多线程环境下,我们需要使用下面线程安全的"懒汉式"单例,其保障线程安全的手段也很简单,直接使用synchronized来修饰getInstance方法。这种办法过于简单粗暴,同时会导致效率十分低下。实例一旦被构造完毕后,由于锁的存在,导致每次只能由一个线程可以获取到实例对象
/**
* 懒汉式
* 虽然达到了按需初始化的目的,但却带来线程不安全的问题
* 可以通过synchronized解决,但也带来效率下降
*/
public class MessageResource03 {
private static MessageResource03 INSTANCE ;
private String description;
/**
* 私有构造器
*
* @param message
*/
private MessageResource03(String message) {
this.description = message;
}
public void getInfo() {
System.out.println(description);
}
/**
* 提供实例的访问接口
*
* @return
*/
public static synchronized MessageResource03 getInstance() {
if(INSTANCE == null){
INSTANCE = new MessageResource03("我是带锁的懒汉式单例模式!");
}
return INSTANCE;
}
public static void main(String[] args) {
MessageResource03 messageResource = MessageResource03.getInstance();
messageResource.getInfo();
}
}
基于DCL(Double-Checked Locking)双重检查锁的单例
通过前面我们看到,无论是饿汉式单例还是懒汉式单例,其都有明显的缺点。那么有没有一种完美的单例?既可以实现Lazy Load延迟加载,又可以在保证线程安全的前提下依然具备较高的效率呢。答案是肯定——基于DCL(Double-Checked Locking)双重检查锁的单例。其实现如下,该单例实现中进行了两次检查。第一次检查时如果发现实例已经构造完毕了,则无需加锁直接返回实例对象即可。其保证了实例在构建完成后,其他多个线程可以同时快速获取该实例。第二次检查时则是为了避免重复构造实例,因为在还未构造实例前,可能会有多个线程通过了第一次检查,准备加锁来构造实例。在DCL的单例实现中,尤其需要注意的一点是静态变量instance必须要使用volatile进行修饰。其原因在于volatile禁止了指令的重排序。这里解释一下为什么要加volatile?
new 一个对象会分为两步,一步初始化,一步赋值(俗称的版初始化状态)。如果不加volatile,那么有可能指令乱序(new对象见上图1,3步骤),这样就有可能在初始化后,直接赋值给INSTANCE,导致下个线程来时,读到不为空的INSTANCE,这样返回的就是初始化值。
/**
* DCL模式
*
* @author Mingchong
* @date 2022年03月19日 11:42
*/
public class MessageResource04 {
//此处必须加volatile修饰
private static volatile MessageResource04 INSTANCE ;
private String description;
/**
* 私有构造器
*
* @param message
*/
private MessageResource04(String message) {
this.description = message;
}
public void getInfo() {
System.out.println(description);
}
/**
* 提供实例的访问接口
*
* @return
*/
public static MessageResource04 getInstance() {
if(INSTANCE == null){
synchronized (MessageResource04.class){
if(INSTANCE == null){
INSTANCE = new MessageResource04("我是DCL双重检查锁单例模式!");
}
}
}
return INSTANCE;
}
public static void main(String[] args) {
MessageResource04 messageResource = MessageResource04.getInstance();
messageResource.getInfo();
}
}
测试结果 |
---|
基于静态内部类的单例
前面我们说到的第一种单例实现,之所以被称为饿汉式、非延迟加载。加载外部类时不会加载内部类,这样可以实现懒加载。同样地,该方式的单例也是满足线程安全的。
/**
* 静态内部类方式
* JVM保证单例
* 加载外部类时不会加载内部类,这样可以实现懒加载
*
*/
public class MessageResource05 {
private String description;
/**
* 私有构造器
*
* @param message
*/
private MessageResource05(String message) {
this.description = message;
}
public void getInfo() {
System.out.println(description);
}
/**
* 提供实例的访问接口
*
* @return
*/
private static class MessageResource05Holder{
private static final MessageResource05 INSTANCE = new MessageResource05("我是静态内部类的单例模式!");
}
public static MessageResource05 getInstance(){
return MessageResource05Holder.INSTANCE;
}
public static void main(String[] args) {
MessageResource05 messageResource = MessageResource05.getInstance();
messageResource.getInfo();
}
}
测试结果 |
---|
基于枚举的单例
对于Java的枚举类型而言,其构造器是且只能是private私有的。故其特别适合用于实现单例模式。下面即是一个基于枚举的单例实现,可以看到此种实现非常简洁优雅。当枚举类进行加载、初始化时,即会完成实例的构建,我们通过枚举的特性保证了实例的唯一性,当然其不是Lazy Load延迟加载的。与此同时根据类的加载机制我们可知其也是线程安全的(由JVM保证),不仅可以解决线程同步,还可以防止反序列化。
/**
* 枚举
* 不仅可以解决线程同步,还可以防止反序列化。
*
*/
public enum MessageResource06 {
/**
* 单例
*/
INSTANCE("我是枚举的单例模式");
private String description;
/**
* 枚举的构造器默认访问权限是private, 当然也只能是私有的
*
* @param message
*/
MessageResource06(String message) {
this.description = message;
}
public void getInfo() {
System.out.println(description);
}
public static void main(String[] args) {
MessageResource06 messageResource = MessageResource06.INSTANCE;
messageResource.getInfo();
}
测试结果 |
---|