什么是单例模式
单例模式(Singleton)也称为单子模式,是一种常见的设计模式,指的是一个类只有一个实例,且该类能自行创建这个实例的一种模式
单例模式使用场景
单例模式其核心在于在整个系统中只创建唯一一个实例,其应用场景主要如下:
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
- 某类需要频繁访问数据库或文件的对象,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
- 网站的计数器(否则难以同步)
- Windows的任务管理器和回收站。
单例模式的代码实现
1.饿汉式
饿汉式顾名思义是饥饿的,因此应该开始就创建对象,代码如下:
public class Singleton_Hungry {
//1:提供一个私有的空参构造器
private Singleton_Hungry(){};
//2:内部提供一个当前类的静态化实例对象
private static Singleton_Hungry singleton_Hungry=new Singleton_Hungry();
//3:提供一个公共的静态方法,返回当前类对象
public static Singleton_Hungry getInstance(){
return singleton_Hungry;
}
}
2.懒汉式
懒汉式顾名思义就是懒,不会跟饿汉式一样上来就创建对象,而是在需要的时候才会创建,而且是仅创建一次,代码如下:
public class Singleton_Lazy {
//1:提供一个私有的空参构造器
private Singleton_Lazy(){};
//2:指向当前类实例的私有静态引用
private static Singleton_Lazy singleton_Lazy;
//3:提供一个公共的静态方法,当需要时才创建当前类对象
public static Singleton_Lazy getInstance(){
if(singleton_Lazy==null){
singleton_Lazy=new Singleton_Lazy();
}
return singleton_Lazy;
}
}
单例模式是否是线程安全的?
1.饿汉式(线程安全)
饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,即在线程访问单例对象之前就已经将对象创建好了,其在整个生命周期中只会随着类的加载而加载一次,所以是线程安全的。
2.懒汉式(不是线程安全的)
懒汉式之所以不是线程安全的主要是由于其中的if判断语句不具有原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。举例来说就是:假设有多个线程1,线程2都需要使用这个单例对象。而恰巧,线程1在判断完singleton_Lazy==null后突然交换了cpu的使用权,变为线程2执行,由于singleton_Lazy仍然为null,那么线程2中就会创建这个singleton_Lazy的单例对象。之后线程1拿回cpu的使用权,而正好线程1之前暂停的位置就是判断singleton_Lazy是否为null之后,创建对象之前。这样线程1又会创建一个新的singleton_Lazy对象。这就违反了单例模式的原则,则其不是线程安全的
如何解决懒汉式不是线程安全的问题?
- 同步延迟加载 — synchronized方法。(同步作用域大、效率低、锁粒度不够精细,不推荐使用)
- 同步延迟加载 — synchronized代码块。(同步作用域较第一种小,但效率跟第一种差不多,不推荐使用)
- 同步延迟加载 — 使用内部类。(效率较高,但是实现较复杂,代码可读性较差,具体视个人编码习惯使用)
- 同步延迟加载 — 双重检查(DCL)。(效率高、实现简单、可读性强、强烈推荐使用,但是一定要使用volatile关键字)
- 同步延迟加载 — ThreadLocal。(效率较第五种低,具体视个人喜好使用)
双重检查具体实现(推荐使用,这里重点讲解这个方案)
具体代码如下:
public class Singleton_Lazy_DOL {
//1:提供一个私有的空参构造器
private Singleton_Lazy_DOL(){};
//2:指向当前类实例的私有静态引用,使用volatile关键字防止重排序
private static volatile Singleton_Lazy_DOL singleton_Lazy;
//3:提供一个公共的静态方法,当需要时才创建当前类对象
public static Singleton_Lazy_DOL getInstance(){
//进行双重检查,只需在第一次创建时才同步,创建成功后就不再需要获取同步锁
if(singleton_Lazy==null){
synchronized (Singleton_Lazy_DOL.class) {
if(singleton_Lazy==null){
singleton_Lazy=new Singleton_Lazy_DOL();
}
}
}
return singleton_Lazy;
}
}
为什么一定要使用volatile关键字?
在java内存模型中,volatile 关键字作用是:
- 保证不同线程对变量操作的内存可见性
- 禁止指令重排序
之前上面说过,因为 singleton_Lazy = new Singleton_Lazy() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
1:给 singleton_Lazy 分配内存空间
2: 调用 Singleton_Lazy 的构造函数等,来初始化 singleton_Lazy
3:将 singleton_Lazy 对象指向分配的内存空间
这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。如果是 1-3-2,那么在第 3 步执行完以后,singleton_Lazy 就不是 null 了,可是这时第 2 步并没有执行,singleton_Lazy 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton_Lazy 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton_Lazy 并没有完成初始化,所以使用这个实例的时候会报错。
因此,一定要使用volatile关键字来避免重排序问题。
最后,关于volatile关键字的一些问题,我在这里分享一篇博主写的文章(不一定是ta的原创):
volatile关键字理解分享链接
单例模式优点
- 由于其在内存中只有一个对象实例,则节省内存空间
- 能避免频繁地创建和销毁对象,提高性能
- 避免对共享资源的多重占用,简化访问操作,如在进行写文件时,由于只有一个实例对象,能避免对同一资源的同时写操作
- 为整个系统提供一个全局访问点,优化和共享资源访问。
注意
建议不要使用反射进行设计单例模式,因为反射可以暴力破坏单例模式,关于反射以及枚举类实现在这里我就不详细说了,直接扔分享连接吧,有兴趣的可以自行查阅。
单例模式大全分享链接
好了,今天的分享就到此结束了,之所以突然想要分享单例模式知识是因为昨天在自学hibernate框架时候,在获取配置文件对象时需要用到。以后在学习过程中遇到的问题我都会尽量整理分享出来,有说的不对的地方还望大家指正。