一、引言
还记得老师当初给我们讲单例模式吗? 小编还清楚记得老师讲了一个是饿汉式一个是懒汉式,也讲了两者的实现方式。
那个时候不理解设计模式是做什么的,就死记硬背记住了,应付一下面试什么的。
如果你只知道两种写法看完文本肯定会有所收获,如果你是大牛,那就可以点点赞什么的哈哈哈哈哈
单例模式使用场景:
如果系统中有比较重量级的对象,并且只需要实例化一个的时候,就考虑使用单例模式。 举个实际例子 ,在实际业务中难免会把数据存储到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、静态内部类、枚举实现方式推荐使用
那么这几种应该是市面上所有的常见的单例模式实现的方式,小编对每一种方式进行分析,也说明了优缺点。小伙伴可以根据不同的业务场景来选择不同的实现方式。