1、简介
1.1、什么是单例模式?
1.2、为什么需要单例模式?
- 节省公共资源,比如数据库连接池,配置等往往伴随着频繁的创建和销毁,为了节省内存的开销,需要把数据库连接池、配置信息设计成单例模式;
-
2、单例模式的实现
单例模式有以下5种实现方式,这5种实现方式都遵循以下设计原则:
单例类的构造函数设计成私有的,避免外部调用单例类的构造函数创建对象;
- 内部有一个
private static类型的实例,当实例在调用getInstance方法时才初始化,称为延迟加载; 向外暴露一个
public static Singleton getInstance方法,该方法最终返回单例对象,外部调用该方法获取单例。2.1、饿汉模式
饿汉模式在声明单例时就对其初始化,即非延迟加载,这样带来的问题是如果单例至始至终没有被使用过,则并不需要初始化单例,会造成内存的浪费。
public class SingletonHungry {private static final SingletonHungry singletonHungry = new SingletonHungry();private SingletonHungry(){};public static SingletonHungry getInstance(){return singletonHungry;}}
2.2、懒汉模式
相比饿汉模式,懒汉模式在单例声明时不初始化,而是在第一次创建单例时才初始化,这就对应了延迟加载。考虑线程安全性,一般对getInstance方法加synchronized关键字同步方法。
public class SingletonLazy {private static SingletonLazy singletonLazy;private SingletonLazy(){};public static synchronized SingletonLazy getInstance(){if (singletonLazy == null){singletonLazy = new SingletonLazy();}return singletonLazy;}}
2.3、双重检查(DCL,Double Check Lock)
懒汉模式通过synchronized同步方法保证线程安全性,但synchronized锁的引入会影响性能,可以使用synchronized代码块来更细粒度地加锁,为了避免指令重排序导致生成的单例没有被初始化,使用volatile关键字修饰单例。
public class SingletonDoubleCheck {private static volatile SingletonDoubleCheck singletonDoubleCheck;private SingletonDoubleCheck(){};public static SingletonDoubleCheck getInstance(){if (singletonDoubleCheck == null){synchronized (SingletonDoubleCheck.class){if (singletonDoubleCheck == null){singletonDoubleCheck = new SingletonDoubleCheck();}}}return singletonDoubleCheck;}}
这里解释两个点:
(1)两次**if (singletonDoubleCheck == null)**分别是为了保证什么?第一个判空是为了提升性能,如果单例已经被创建了,直接
return就可以了,不必再走进synchronized代码块里了,其实只要锁住synchronized代码块里的部分就ok了,这一步只是为了提升性能;- 第二个判空是为了确保仅当单例对象第一次调用时被初始化,即保证单例仅初始化一次。具体地,由于getInstance方法本身没有加synchronized,假设线程1先进入第一个if判断,通过后获取同步锁,还没有执行单例的初始化,此时第二个线程调用getInstance方法,线程2也通过了第一个if判断,然后进入阻塞状态,然后线程1执行完单例的初始化释放掉同步锁,线程2获取同步锁进入同步代码块,如果没有第二个判空,此时线程2会执行单例的初始化,这样就违背了单例只能进行一次初始化的定义,因此需要加上第二个if判空,且将单例用volatile关键字修饰,避免线程2又一次进行单例的初始化。
(2)为什么要用**volatile**关键字修饰单例?
根因是singletonDoubleCheck = new SingletonDoubleCheck();这个操作不是原子性的!Object obj = new Object();实际分为四步(一般把2、3合成一步):
- 在栈区创建一个指向
Object对象的引用obj; - 在堆区开辟一块内存空间给
Object对象; - 执行构造函数初始化
Object对象; - 将
obj引用指向2中创建的Object对象。
指令重排序会使线程按1、2、3、4执行,也可能按1、4、2、3执行,假设线程1执行了1、2、4,即此时引用obj指向了一块内存空间,并不是null了,这时线程1触发异常退出synchronized代码块并释放锁,线程2进来,由于此时obj不是null了,会直接返回单例,但这时单例并没有走步骤3进行初始化,可能会影响业务逻辑。因此使用volatile关键字,禁止指令重排序,保证返回的单例都是执行了构造函数进行初始化了的。
2.4、静态内部类单例模式
静态内部类单例模式,也是最推荐使用的单例模式,他的工作原理如下:
- 当任何一个线程第一次调用
getInstance()时,都会使LazyHolder静态内部类被加载,此时静态初始化器将执行Singleton的初始化操作,也是延迟加载; 初始化静态数据时,Java提供了的线程安全性保证。(所以不需要任何的同步)
public class SingletonInner {private SingletonInner(){};public static SingletonInner getInstance(){return LazyHolder.INSTANCE;}private static class LazyHolder{private static SingletonInner INSTANCE = new SingletonInner();}}
2.5、枚举单例模式
应该是最简洁的单例模式实现方案了,是借助了枚举类已有的实现和机制,优点在于:
写法简单;
- 防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候(安全)!(不是很理解)。
枚举方式实现单例模式:
public enum SingletonEnum {SINGLETON_ENUM;}
测试Demo:
public class Main {public static void main(String[] args) {SingletonEnum singletonEnum1 = SingletonEnum.SINGLETON_ENUM;SingletonEnum singletonEnum2 = SingletonEnum.SINGLETON_ENUM;SingletonEnum singletonEnum3 = SingletonEnum.SINGLETON_ENUM;System.out.println(singletonEnum1.hashCode());System.out.println(singletonEnum2.hashCode());System.out.println(singletonEnum3.hashCode());}}
