单例模式
单例模式
概述
- 定义 : 确保一个类只有一个实例,并为整个系统提供一个全局访问点。
- 类型: 创建模式
- 结构:
1、单线程下的单例模式实现
立即加载 :** 在类加载初始化的时候就主动创建实例;
延迟加载 : 等到真正使用的时候才去创建实例,不用时不去主动创建。
在单线程环境下的两种经典的实现: 饿汉式单例(立即加载)、懒汉式单例(延迟加载)
饿汉式单例
// 饿汉式单例
public class Singleton1 {
// 指向自己实例的私有静态引用,主动创建
private static Singleton1 singleton1 = new Singleton1();
// 私有的构造方法
private Singleton1(){}
// 以自己实例为返回值的静态的公有方法,静态工厂方法
public static Singleton1 getSingleton1(){
return singleton1;
}
}
类加载的方式是按需加载,且加载一次。。这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,在JVM层面上执行一次,所以饿汉式 在单/多线程环境下都是天生线程安全。
测试:开10个线程打印hashCode值, 结果都是一样的。
/**
*
*@author Saiuna
*@date 2020/7/6 12:45
*/
public class test {
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] =new Thread(()->{
System.out.println(Singleton1.getInstance().hashCode());
});;
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
}
}
懒汉式单例
/**
*传统的懒汉模式,线程不安全
*@author Saiuna
*@date 2020/7/6 12:54
*/
public class Singleton2 {
private static Singleton2 singleton;
private Singleton2() { }
public static Singleton2 getInstance() {
// 被动创建,在真正需要使用时才去创建
if (singleton == null) {
singleton = new Singleton2();
}
return singleton;
}
}
实例被延迟加载,用到时才会实例化一个对象并交给自己的引用。
注:懒汉式单例本身非线程安全的,多个线程同时访问会产生多个实例 (用上述测试案例测试会得到不同的hashCode)。至于详细的为什么不安全,就涉及到多线程的问题了。
多个线程同时竞争下, singleton的值对于其他线程是不可见的,会发生多个线程同时判断singleton都为null值,从而产生了多个实例。
2、多线程下的单例模式实现
懒汉式单例本身是非线程安全的,在多线程环境中很可能会产生多个实例。那如何保证线程安全呢
2.1 内部类-延迟加载
/**
*懒汉模式,使用内部类实现延迟加载
*@author Saiuna
*@date 2020/7/6 14:11
*/
public class Singleton5 {
private static class Holder {
private static Singleton5 singleton5 = new Singleton5();
}
private Singleton5() {
}
public static Singleton5 getInstance() {
return Holder.singleton5;
}
}
使用内部类实现线程安全的懒汉式单例,也是一种效率比较高的做法, 为什么安全呢~与饿汉单例同理。
2.2 双重检查(Double-Check idiom)
/**
*线程安全的懒汉式单例---双重检查(Double-Check idiom)
*@author Saiuna
*@date 2020/7/6 14:21
*/
public class Singleton6 {
/**
* 使用volatile关键字防止重排序,因为 new Instance()是一个非原子操作,可能创建一个不完整的实例
*/
private static volatile Singleton6 singleton6;
private Singleton6() {
}
public static Singleton6 getInstance() {
// Double-Check idiom 第一次非 null检查,避免频繁上锁
if (singleton6 == null) {
synchronized (Singleton6.class) {
// 只需在第一次创建实例时才同步
if (singleton6 == null) {
singleton6 = new Singleton6();
}
}
}
return singleton6;
}
}
这里必须用volatile 来修饰 实例,恰好写过一篇关于volatile 的文章,对此理解起来更加容易
volatile : 禁止指令重排序(实现: 内存屏障)
那么,如果不用 volatile 修饰 singleton6,会发生什么情形呢?
(1)当我们写了 new 操作,JVM 到底会发生什么?
首先,我们要明白的是: **new Singleton6() 是一个非原子操作。**代码行singleton6= new Singleton6(); 的执行过程可以形象地用如下3行伪代码来表示:
- memory = allocate(); //1:分配对象的内存空间
- ctorInstance(memory); //2:初始化对象
- singleton3 = memory; //3: 使singleton6指向刚分配的内存地址
**但实际上,这个过程可能发生无序写入(指令重排序),也就是说上面的3行指令可能会被重排序导致先执行第3行后执行第2行,**也就是说其真实执行顺序可能是下面这种:
- memory = allocate(); //1:分配对象的内存空间
- singleton3 = memory; //3:使singleton6指向刚分配的内存地址
- ctorInstance(memory); //2:初始化对象
如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,此时别的线程将得到的是一个不完整(未初始化)的对象,后果可想而知。
单例模式的优点
- 在内存中只有一个对象,节省内存空间;
- 避免频繁的创建销毁对象,可以提高性能;
- 避免对共享资源的多重占用,简化访问;
- 为整个系统提供一个全局访问点。
单例模式的使用场景
单例模式是比较常用的一种设计模式,其核心在于为整个系统提供一个唯一的实例,其应用场景如:
- 有状态的工具类对象;
- 频繁访问数据库或文件的对象;
- 国际化(翻译缓存,频繁用于翻译)
- …
饿汉式 懒汉式的比较
从速度和反应时间角度来讲,饿汉式要好一些;从资源利用效率上说,懒汉式要好一些。
饿汉式:
- 天生线程安全,使用简单。
- 速度和反应时间角度上好于懒汉。
懒汉式:
- 资源利用效率好。
- 但要注意线程安全问题
总结:
-
一般建议用饿汉式,天生线程安全,(再者思考一个问题:不用的话写来干嘛!),除非真的资源紧张,注重资源的场景的话还是需要用到懒汉式的。
-
在使用单例模式时,我们必须使用单例类提供的公有工厂方法得到单例对象,而不应该使用反射来创建,否则将会实例化一个新对象
-
这边只是举例了比较常用的案例,用枚举和ThreadLoad也可以实现, 其中《Effective Java》中就是推荐用枚举~只是有个印象(●’◡’●)
参考《Java并发编程的艺术》