设计模式-创建型模式-单例模式
一、什么是单例模式
单例模式,属于创建类型的一种常用的设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)。
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。(摘自百度百科)
单例模式的应用场景有很多:
1、数据库连接池的设计与实现;
2、多线程的线程池设计与实现;
3、Spring中创建的Bean实例默认都是单例;
4、Java-Web中,一个Servlet类只有一个实例……
二、单例模式实现
2.1、饿汉模式
public class SingularDemo3 {
private static SingularDemo3 instance = new SingularDemo3();
private SingularDemo3() {
System.out.println("SingularDemo3");
}
public static SingularDemo3 getInstance(){
// 在程序启动的时候直接运行加载,后续有外部需要使用的时候获取即可
// 导致的问题就像你下载个游戏软件,可能你游戏地图还没有打开呢,但是程序已经将这些地图全部实例化
return instance;
}
}
可以看到上述代码,很简单,直接在启动时加载,但是导致的就是,很多没用到的东西,他也给加载了,造成不必要的负担。
2.2、懒汉模式
2.2.1、使用synchronized锁
public class SingularDemo2 {
public static volatile SingularDemo2 instance;
private SingularDemo2() { // 设置成私有,不允许外面调用
System.out.println("SingularDemo2");
}
public static synchronized SingularDemo2 getInstance(){
if(instance != null) return instance;
return new SingularDemo2();
}
}
这种方式,是不用不加载,用到的时候再加载,使用synchronized加锁,保证只有一个线程访问,这种是线程安全的。但锁的粒度太大,导致竞争激烈,造成不必要的资源浪费。
同时这里的volatile,是防止指令重排序,导致导致对象未初始化完全。
2.2.2、使用双重校验锁
public class SingularDemo5 {
public static volatile SingularDemo5 instance; // 要用volatile修饰,防止指令重排序
private SingularDemo5() {
System.out.println("singularDemo5");
}
public static SingularDemo5 getInstance(){
if(instance != null) return instance; // 如果有,直接返回
synchronized (SingularDemo5.class){ // 锁住此对象
if(instance != null) return instance; // 进入之后再判断一次
instance = new SingularDemo5();
return instance;
}
}
}
双重校验锁,降低了锁的粒度,只有当instance没有被初始化的时候,第一个访问的线程才回去实例化它。这里内部还要有一个判断instance是否为null,是为了防止同时多个线程竞争,一个线程初始化instance之后,其他线程也竞争到锁,再去初始化instance,所以进入临界区之后第一件事就是看看是否已经初始化好了。
这里的volatile有两个作用,一是保证可见性,其他线程读取的都是最新值;二是防止指令重排序导致对象未初始化完全。
2.2.3、类的内部类
public class SingularDemo4 {
public static class Handle{
public static SingularDemo4 instance = new SingularDemo4();
}
public SingularDemo4() {
System.out.println("SingularDemo4");
}
public static SingularDemo4 getInstance(){
return Handle.instance;
}
}
类的内部类是靠JVM来保证并发的正确性,也就是一个类的构造方法在多线程下可以被正常的加载。我们永远可以相信JVM。
那问题来了?JVM是如何保证多线程下类的构造方法只会被执行一次的呢?
1、类加载机制和初始化锁
当类加载器加载类时,JVM会使用类加载锁(Initialization Lock)来确保在同一时刻只有一个线程对类进行加载和初始化操作。
一个线程开始初始化一个类时,其他线程需要等待这个类初始化完成,这是因为类的初始化过程中可能会执行静态代码块或静态变量赋值等操作,保证这些操作的互斥性能够避免多线程环境下的干扰。
2、Happens-Before
在类的初始化过程中,对静态变量的赋值操作在对象引用发布之前必须完成,这意味着在静态变量赋值完成之前,其他线程不会看到不完整或部分初始化的对象。
2.2.4、使用CAS
public class SingularDemo7 {
private static final AtomicReference<SingularDemo7> instance = new AtomicReference<>();
private SingularDemo7(){
System.out.println("singularDemo7");
}
public static SingularDemo7 getInstance() {
while (true) {
SingularDemo7 singularDemo7 = instance.get();
if(singularDemo7 != null) return singularDemo7;
instance.compareAndSet(null, new SingularDemo7());
return instance.get();
}
}
}
使用CAS方式保证单例,好处是不用加锁,效率更高;缺点也很明显,竞争激烈的情况下,可能大家都没法玩,一直循环,一直失败重试……
2.2.5、使用枚举类
public enum SingularDemo8 {
instance;
}
Effective Java 作者推荐使用枚举的方式解决单例模式。
三、小结
单例模式在实际的开发中应用的很多,它确保一个类只有一个实例,且这个实例的构造方法是私有的,并提供一个全局访问点来获取该实例。
实现方式大致可以分为饿汉类和懒汉类,饿汉就是程序启动时就加载,这样有可能导致没有被用到的类也加载了,导致了性能的浪费;懒汉式的加载是用到就加载,不用不加载,但是要注意的是线程安全问题,合理的使用synchronized和volatile来保证线程安全。
推荐使用的方式是双重校验锁和类的内部类,这两种方式既能保证懒加载,也能保证不会因为加锁或者锁的粒度较大导致的性能浪费。