最全23种设计模式之单例模式(Singleton)
Posted 陈皮的JavaLib
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最全23种设计模式之单例模式(Singleton)相关的知识,希望对你有一定的参考价值。
23种设计模式传送门
23种设计模式,会持续讲解,关注收藏
,带你深入学习23种设计模式。
目录
1 场景分析
假设开发一个项目,会调用第三方系统的接口,并且需要用到第三方系统提供的相关配置参数信息,而这些参数信息配置在配置文件中,程序需要读取到内存中使用。
比如配置文件是采用 properties 格式的,内容如下:
ThirdApp.properties
appId=188210
secret=MIVD587A12FE7E
一般实现,是直接读取配置文件的信息,然后存储在一个对象中,需要时读取这个对象中的数据即可。
package com.nobody.singleton;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* @Description 第三方系统相关配置信息
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public class ThirdConfig {
/**
* 应用ID
*/
private String appId;
/**
* 密钥
*/
private String secret;
public ThirdConfig() {
// 初始化
init();
}
/**
* 读取配置文件,进行初始化
*/
private void init() {
Properties p = new Properties();
InputStream in = ThirdConfig.class.getResourceAsStream("ThirdApp.properties");
try {
p.load(in);
this.appId = p.getProperty("appId");
this.secret = p.getProperty("secret");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != in) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 省略get/set方法
}
简单测试一下,示例如下:
package com.nobody.singleton;
/**
* @Description
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public class Main {
public static void main(String[] args) {
ThirdConfig thirdConfig = new ThirdConfig();
System.out.println("appId=" + thirdConfig.getAppId());
System.out.println("secret=" + thirdConfig.getSecret());
}
}
// 输出结果如下
appId=188210
secret=MIVD587A12FE7E
通过以上分析,我们需要使用配置信息时,只要获取 ThirdConfig 类的实例即可,但是如果项目是多人协作的,其他人使用这个配置信息的时候选择每次都去 new 一个新的实例,这样就会导致每次都会生成新的实例,并且重复读取配置文件的信息,不仅多次造成IO操作,而且系统中会存在多个 AppConfig 实例对象,严重浪费内存资源,如果配置文件内容多的话,浪费系统资源更加严重。
针对以上问题,因为配置信息是不变的,共享的,所以我们只要保证系统运行期间,只有一个类实例存在就可以了。而且单例模式就能解决以上问题。
2 单例模式(Singleton)
在系统运行期间,一个类仅有一个实例,并提供一个对外访问这个实例的全局访问点。
如果一个类的构造方法不对外公开,仅让类自身来负责自己类实例的创建工作,并且只创建一个实例,然后提供一个对外访问这个实例的方法,这就是单例模式的实现方式。
在 Java 中,单例模式的实现一般分为两种,一种称为懒汉式,一种称为饿汉式,它们之间主要的区别是在创建实例的时机上,一种是提前创建实例,一种是使用时才创建实例。
2.1 饿汉式
饿汉式、顾名思义,很饥饿很着急,所以在类加载器装载类的时候就创建对象实例,由JVM保证线程安全,只创建一次,饿汉式实现示例代码如下:
package com.nobody.singleton;
/**
* @Description 饿汉式单例模式
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public class HungerSingleton {
/**
* 存储创建好的类实例,提前实例化,并且由JVM保证创建一次
* 使用static关键字修饰,使它在类加载器装载后,初始化此变量,并且能让静态方法getInstance使用
*/
private static HungerSingleton instance = new HungerSingleton();
/**
* 私有化构造方法,只能内部调用,外部调用不了则避免了多次实例化的问题
*/
private HungerSingleton() {}
/**
* 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
*
* @return HungerSingleton 实例
*/
public static HungerSingleton getInstance() {
return instance;
}
}
2.2 懒汉式
懒汉式、顾名思义,既然懒就不着急,即等到要使用对象实例的时候才创建实例,更准确的说是延迟加载。懒汉式实现示例代码如下:
package com.nobody.singleton;
/**
* @Description 懒汉式单例模式
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public class LazySingleton {
/**
* 存储创建好的类实例,赋值null,使用时才创建赋值
* 因为静态方法getInstance使用了此变量,所以使用static关键字修饰
*/
private static LazySingleton instance = null;
/**
* 私有化构造方法,只能内部调用,外部调用不了则避免了多次实例化的问题
*/
private LazySingleton() {}
/**
* 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
*
* @return LazySingleton 实例
*/
public static LazySingleton getInstance() {
// 判断对象实例是否已被创建
if (instance == null) {
// 第一次使用,没用被创建,则先创建对象,并且存储在类变量中
instance = new LazySingleton();
}
// 如果已创建过,则直接使用
return instance;
}
}
3 优缺点比较
饿汉式,当类装载的时候就创建类实例,不管以后会不会使用到,以后使用的时候直接获取即可,不需要判断是否已经创建,节省时间,典型的空间换时间。
懒汉式,等到使用的时候才创建类实例,每次获取实例都要进行判断是否已经创建,浪费时间,但是如果未使用前,则能达到节约内存空间的效果,典型的时间换空间。
从线程安全性上讲, 饿汉式是类加载器加载类到JVM内存中后,就实例化一个这个类的实例对象,由JVM保证了线程安全。而不加同步的懒汉式是线程不安全的,在并发的情况下可能会创建多个实例。
如果有两个线程A和B,它们同时调用 getInstance 方法时,可能导致并发问题,如下:
/**
* 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
*
* @return LazySingleton 实例
*/
public static LazySingleton getInstance() {
// 判断对象实例是否已被创建
if (instance == null) {
// 线程A运行到这里了,正准备创建实例,或者实例还未创建完,
// 此时线程B判断instance还是为null,则线程B也进入此,
// 最终线程A和B都会创建自己的实例,从而出现了多实例
instance = new LazySingleton();
}
// 如果已创建过,则直接使用
return instance;
}
所以我们一般推荐饿汉式单例模式,因为由JVM实例化,保证了线程安全,实现简单。而且这个实例总会用到的时候,提前实例化准备好也未尝不可。
4 高级实现
4.1 双重检查加锁
前面说到,懒汉式单例模式在并发情况下可能会出现线程安全问题,那我们可以通过加锁,保证只能一个线程去创建实例即可,只要加上 synchronized 即可,如下所示:
public static synchronized LazySingleton getInstance() {
// 判断对象实例是否已被创建
if (instance == null) {
// 第一次使用,没用被创建,则先创建对象,并且存储在类变量中
instance = new LazySingleton();
}
// 如果已创建过,则直接使用
return instance;
}
如果对整个方法加锁,会降低访问性能,即每次都要获取锁,才能进入执行方法。可以使用双重检查加锁
的方式来实现,就可以既实现线程安全,又能够使性能不受到很大的影响。
双重检查加锁机制:每次进入方法不需要同步,进入方法后,先检查实例是否存在,如果不存在才进入加锁的同步块,这是第一重检查。进入同步块后,再次检查实例是否存在,如果不存在,就在同步的
情况下创建一个实例,这是第二重检查。
使用双重检查加锁机制时,需要借助 volatile 关键字,被它修饰的变量,变量的值就不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,所以能确保多个线程能正确的处理该变量。
package com.nobody.singleton;
/**
* @Description 懒汉式单例模式
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public class LazySingleton {
/**
* 存储创建好的类实例,赋值null,使用时才创建赋值
* 因为静态方法getInstance使用了此变量,所以使用static关键字修饰
*/
private volatile static LazySingleton instance = null;
/**
* 私有化构造方法,只能内部调用,外部调用不了则避免了多次实例化的问题
*/
private LazySingleton() {}
/**
* 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
*
* @return LazySingleton 实例
*/
public static synchronized LazySingleton getInstance() {
// 第一重检查,判断实例是否存在,如果不存在则进入同步块
if (instance == null) {
// 同步块,能保证同时只能有一个线程访问
synchronized (LazySingleton.class) {
// 第二重检查,保证只创建一次对象实例
if (instance == null) {
instance = new LazySingleton();
}
}
}
// 如果已创建过,则直接使用
return instance;
}
}
4.2 静态内部类实现
有一种方式,既有饿汉式的优点(线程安全),又有懒汉式的优点(延迟加载),那就是使用静态内部类。
何为静态内部类呢?即在类中定义的并且使用 static 修饰的类。可以认为静态内部类是外部类的静态成员,静态内部类对象与外部类对象间不存在依赖关系,因此可直接创建。
静态内部类中的静态方法可以使用外部类中的静态方法和静态变量;而且静态内部类只有在第一次被使用的时候才会被装载,达到了延迟加载的效果。然后我们在静态内部类中定义一个静态的外部类的对象,并进行初始化,由JVM保证了线程安全,进行创建。
package com.nobody.singleton;
/**
* @Description 静态内部类实现单例模式
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public class StaticInnerClassSingleton {
/**
* 静态内部类
*/
private static class SingletonHolder {
/**
* 静态初始化,由JVM保证线程安全
*/
private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}
/**
* 私有化构造方法
*/
private StaticInnerClassSingleton() {}
/**
* 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
*
* @return StaticInnerClassSingleton 实例
*/
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.instance;
}
}
4.3 枚举实现
还有一种单例模式的最佳实现,就是借助枚举。实现简洁,而且无偿地提供了序列化的机制,并由 JVM 从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。实例代码如下:
package com.nobody.singleton;
/**
* @Description 枚举实现单例模式
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public enum EnumSingleton {
/**
* 定义一个枚举的元素,代表要实现类的一个实例
*/
instance;
}
如果要使用,直接使用即可,如下:
package com.nobody.singleton;
/**
* @Description
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public class Main {
public static void main(String[] args) {
EnumSingleton enumSingleton = EnumSingleton.instance;
}
}
5 指定数量实例模式
单例模式,只有一个类实例。如果要求指定数量的类实例,例如指定2个或者3个,或者任意多个?
其实万变不离其宗,单例模式是只创建一个类实例,并且存储下来。那指定数量多例模式,就创建指定的数量类实例,也存储下来,使用时根据策略(例如轮询)取指定的实例即可。以下演示指定5个实例的情况:
package com.nobody.singleton;
import java.util.HashMap;
import java.util.Map;
/**
* @Description 指定数量的多实例模式
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public class ExtendSingleton {
/**
* 指定的实例数
*/
private final static int INSTANCE_NUM = 5;
/**
* key前缀
*/
private final static String PREFIX_KEY = "instance_";
/**
* 缓存实例的容器
*/
private static Map<String, ExtendSingleton> map = new HashMap<>();
/**
* 记录当前正在使用第几个实例
*/
private static int num = 1;
/**
* 私有化构造方法,只能内部调用,外部调用不了则避免了多次实例化的问题
*/
private ExtendSingleton() {}
/**
* 外部访问这个类实例的方法,使用static关键字修饰,则外部可以直接通过类来调用这个方法
*
* @return ExtendSingleton 实例
*/
public static ExtendSingleton getInstance() {
// 缓存key
String key = PREFIX_KEY + num;
// 优先从缓存中取
ExtendSingleton extendSingleton = map.get(key);
// 如果指定key的实例不存在,则创建,并放入缓存容器中
if (extendSingleton == null) {
extendSingleton = new ExtendSingleton();
map.put(key, extendSingleton);
}
// 把当前实例的序号加1
num++;
if (num > INSTANCE_NUM) {
// 如果实例的序号已经达到最大值了,那就重复从1开始获取
num = 1;
}
return extendSingleton;
}
}
以上是关于最全23种设计模式之单例模式(Singleton)的主要内容,如果未能解决你的问题,请参考以下文章