一、引言
还记得老师当初给我们讲单例模式吗? 小编还清楚记得老师讲了一个是饿汉式一个是懒汉式,也讲了两者的实现方式。
那个时候不理解设计模式是做什么的,就死记硬背记住了,应付一下面试什么的。
如果你只知道两种写法看完文本肯定会有所收获,如果你是大牛,那就可以点点赞什么的哈哈哈哈哈
单例模式使用场景:
如果系统中有比较重量级的对象,并且只需要实例化一个的时候,就考虑使用单例模式。 举个实际例子 ,在实际业务中难免会把数据存储到Elasticsearch(搜索引擎),那么对于操作Elasticsearch来说,只需要实例化一个对象即可,这个对象负责与Elasticsearch进行数据交互,看下实际代码如下:
一个很简单的饿汉式单例模式,把实例化的过程写在静态块当中,根据不同的启动环境连接不同的地址的Elasticsearch,最后创建对象赋值给esOperateService。
/**
* @Description: Elasticsearch 代理对象,使用单例模式饿汉式 - 静态块实现
*/public class EsServiceProxy {
private static EsOperateService esOperateService;
// 私有构造,防止外部new
private EsServiceProxy() {
}
// 提供获取实例的静态方法
public static EsOperateService getEsOperateService() {
return esOperateService;
}
static {
String env = Foundation.server().getEnvType();
if (StringUtils.isEmpty(env)) {
throw new RuntimeException("环境变量env未配置,请检查配置!");
}
env = env.toLowerCase();
String baseurl = "#;;
if (!env.equals("dev") && !env.equals("fat")) {
if (env.equals("uat")) {
baseurl = "#;;
} else if (env.equals("pro")) {
baseurl = "#;;
}
}
// 通过代理工厂创建对象并且赋值给常量
HessianProxyFactory hessianProxyFactory = new HessianProxyFactory();
hessianProxyFactory.setOverloadEnabled(true);
try {
esOperateService = (EsOperateService) hessianProxyFactory.create(EsOperateService.class, baseurl);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
二、单例模式饿汉式 – 静态常量
饿汉式在对象实例化的时候,就会创建好对象,没有达到懒加载的效果( 懒加载:就是说这个对象我不要一开始就创建,等到我需要用的时候在创建 ),但是这样就不会因为多线程的问题导致创建多个实例。
如果保证这个对象,在系统中一定会有用到,那么这种方式也是推荐使用的。
/**
* @Description: 单例模式 饿汉式 - 静态常量
*
* 优点:写法比较简单,在类装载的时候就完成实例化,避免了多线程的问题
* 缺点:这种方式没有达到懒加载的效果,可能会造成内存浪费 (如果系统中一定会用到这个对象,则就避免了内存浪费)
*
*/public class SingletonCase {
// 私有构造方法,避免外部new
private SingletonCase() {
}
// 创建静态常量
private static final SingletonCase singleton = new SingletonCase1();
// 给外部提供实例获取方法
public static SingletonCase getSingleton() {
return singleton;
}
}
三、单例模式饿汉式 – 静态代码块
如果保证这个对象,在系统中一定会有用到,那么这种方式也是推荐使用的。
/**
* @Description: 单例模式 饿汉式 - 静态代码
*
* 优缺点和饿汉式静态常量一致
*
*/public class SingletonCase {
// 私有构造方法,避免外部new
private SingletonCase() {
}
// 创建静态常量
private static SingletonCase singleton;
static {
// 将对象的实例化放在静态块当中,则可以编写一些逻辑代码,如文章一开始举的实战例子
singleton = new SingletonCase();
}
// 给外部提供实例获取方法
public static SingletonCase getSingleton() {
return singleton;
}
}
四、单例模式懒汉式 – 线程不安全写法
懒汉式:可以这样去记忆理解,既然是懒汉式就是在类加载的时候,懒得去创建对象,等到要用的时候在创建,这样也就实现了懒加载的效果。 但是这样实现也会存在一个很严重的问题,那就是在多线程的情况下,会存在创建多个实例的现象,具体看如下实现代码。
这种方式不推荐使用
/**
* @Description: 单例模式 懒汉式 - 线程不安全
*
* 优点:可以实现懒加载的效果
* 缺点:在多线程的情况,可能会创建多个实例的情况
*
*/public class SingletonCase {
// 私有构造方法,避免外部new
private SingletonCase() {
}
// 创建静态常量
private static SingletonCase singleton;
// 给外部提供实例获取方法
public static SingletonCase getSingleton() {
// 这里会存在多线程的问题,假设线程一正在执行new SingletonCase()的操作,此时的singleton还是为null
// 线程二进行 singleton == null 判断,这个时候等式还是成立的,所以线程二也会执行创建对象的操作。
if (singleton == null) {
singleton = new SingletonCase();
}
return singleton;
}
}
、
五、单例模式懒汉式 – 线程安全、同步方法
这个是针对上面的懒汉式进行了改进,给方法加上了synchronized关键字,能给有效 的 解决多线程的问题,但是会影响执行效率,因为所有的线程都要排队执行,所以这种方式也 不推荐使用 。
/**
* @Description: 单例模式 懒汉式 - 线程不安全
*
* 优点:可以解决多线程的问题
* 缺点:执行效率太慢,因为加上synchrionzed所有线程将会排队等待
*
*/public class SingletonCase {
// 私有构造方法,避免外部new
private SingletonCase() {
}
// 创建静态常量
private static SingletonCase singleton;
// 给外部提供实例获取方法
// 这里给方法上加了synchronized关键字,能够保证只有一个线程执行,其他线程排队等待
public static synchronized SingletonCase getSingleton() {
if (singleton == null) {
singleton = new SingletonCase();
}
return singleton;
}
}
六、单例模式 – 双重检查
经过分析懒汉式,一共存在两个问题:1 多线程会创建多个、2、执行效率的问题
针对以上两个问题,所以就有了双重检查,这种方式不仅仅避免多线程的问题,还不会影响效率,也实现了懒加载,在公司中也比较常用。
这种方式推荐使用
/**
* @Description: 单例模式 双重检查
*
* 优点:推荐使用,能够解决懒加载、多线程的问题
*
* volatile :
*、保证此变量对所有线程的可见性,“可见性”指当一条线程修改了这个变量的值,新的值对与其他线程来说是立即得知的。
*、禁止指令重排序优化。
*
*/public class SingletonCase {
// 私有构造方法,避免外部new
private SingletonCase() {
}
/**
*
* 加了volatile关键字,能够解决指令重排的问题,这里简单的解释一下指令重排的问题,需要一点点java内存模型的基础
*
* 执行这句代码时 singleton = new SingletonCase();
*
* 正常情况下是这样的指令顺序
*、memory = allocate() 分配对象的内存空间
*、ctorInstance() 初始化对象
*、instance = memory 设置instance指向刚分配的内存
*
* 在多线程的情况下,JVM 和 CPU优化会发生指令重排,变成这样了
*、memory = allocate() 分配对象的内存空间
*、instance = memory 设置instance指向刚分配的内存
*、ctorInstance() 初始化对象
*
* A线程在new SingletonCase()的时候,如果在指令重排以后的情况下,执行到以上的步骤三时,这个时候对象还未初始化
* B线程进执行第一个if判断的时候,则会直接返回对象,这个时候对象还是未初始化的,如果直接使用则会出现问题
*
*/
// 创建静态常量
// 这里给常量加了volatile关键字,能够保证此变量对所有线程对可见性
// 当只要有一个线程修改了这个变量的值,那么其他线程也就可以立马获取到改变之后的值。
private static volatile SingletonCase singleton;
// 给外部提供实例获取方法
public static SingletonCase getSingleton() {
// 有些小伙伴在这里有点疑问,这里也加了synchronized关键字呀,为什么不会影响效率呢?
// 小伙伴可以仔细看下代码,假设线程一进来之后,通过了第一个if判断,然后进入下面的代码,并且锁住了,然后创建了对象
// 然后线程二进来,即使通过了第一个if判断,等线程一执行完,此时的singleton已不再为空,所以避免了创建多个对象
// 那之后的线程如果再进来,直接在第一个if判断就不通过了,不会有线程等待的现象,也不影响效率。
if (singleton == null) {
synchronized (SingletonCase.class) {
if (singleton == null) {
singleton = new SingletonCase();
}
}
}
return singleton;
}
}
七、单例模式 – 静态内部类
这种方式也是可以 推荐使用 的,也避免了之前懒汉式的问题,编码也比较简单。
利用了jvm在装载类的时候,线程是安全的,也利用了内部类的在加载类时,不会加载内部类,所以这样写也是一种方式。
/**
* @Description: 单例模式 懒汉式 - 静态内部类
*
* 优点:
*、静态内部类在类加载的时候,是不会被加载的,实现了懒加载的特性
*、在调用获取实例的方法是,会去装载内部类,在jvm装载类的时候线程是安全的,静态属性只会在装载类初始化一次
*
*/public class SingletonCase {
// 私有构造方法,避免外部new
private SingletonCase() {
}
// 创建静态内部类,提供常量
private static class SingletonInstance {
private static final SingletonCase SINGLETON = new SingletonCase6();
}
// 给外部提供实例获取方法
public static SingletonCase getSingleton() {
return SingletonInstance.SINGLETON;
}
}
八、单例模式 – 枚举实现
枚举的实现方式确实能够达到单例模式所期待的效果,但小编在工作当中也没有遇到过实际的使用场景。
这种实现方式也不存在有什么问题,所以也是值得被 推荐使用
/**
* @Description: 单例模式 懒汉式 - 枚举实现
* <p>
* 优点:借助JDK枚举来实现单例模式,不仅能避免多线程同步的问题,而且还能防止反序列化重新创建新的对象
*/public class SingletonCase {
// 私有构造方法,避免外部new
private SingletonCase() {
}
// 给外部提供实例获取方法
public static SingletonCase getSingleton() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonCase singletonCase8;
// JVM 保证了这个方法绝对只调用一次
Singleton() {
singletonCase = new SingletonCase8();
}
public SingletonCase getInstance() {
return singletonCase;
}
}
}
九、总的来说
值得推荐使用的几种方式有:
1、如果保证这个对象在项目中一定有使用到,那么饿汉式是值得推荐使用的,在项目中比较常用。
2、双重检查方式推荐使用,在项目中比较常用。
3、静态内部类、枚举实现方式推荐使用
那么这几种应该是市面上所有的常见的单例模式实现的方式,小编对每一种方式进行分析,也说明了优缺点。小伙伴可以根据不同的业务场景来选择不同的实现方式。