前言
本文在实现标题中包括了一些jvm、多线程的小知识,如果还没学的部分可以先跳,也可以去了解。
什么是单例模式
单例模式,是一种常见的创建型设计模式。单例模式将构造器私有化(防止其他对象实例化该对象),并且需要创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例,这个应该是参考ThreadLocal),并提供给其他对象一个能获取自己的唯一对象的方法。
适用场景
-
频繁IO资源的对象。 比如你的MySQL配置文件,经典的四个属性url、username、password、driver,通过properties将属性读取到你的配置类,而读取properties文件到配置类是一个IO过程(从磁盘文件中读取,写入内存),挺耗CPU资源的。
-
创建对象时耗时过多或者耗资源过多,但是又经常用到的共享资源对象。(数据库连接池在连接前需要去配置类中拿数据,而你这个配置类的数据又很常用,但这个常用是共享资源哈。)
-
频繁实例化和销毁,也就是频繁的new对象,并且要求对象不多变,要多变也必须整个系统共享,可以考虑单例模式。
分类
总的分成两大类,饿汉式和懒汉式
对比来看:
- 饿汉式都是线程安全的,而懒汉式需要看情况
- 懒汉式可以起到一个懒加载的作用,也就是说到需要使用的时候才实例化,这样的好处就是节省空间资源。而饿汉式是在类加载阶段就已经将实例对象创建了。 总结一句话来说,饿汉式是在类加载时就实例化了,懒汉式是在需要使用类时再实例化。
饿汉式(都是安全的)
在类常量直接实例化/静态代码块实例化(其实都是一样)
饿汉式即单例对象在类加载时实例化,并赋值给static变量(这个类加载指的是类的生命周期五个过程中的加载、链接、初始化前三个过程,而不是class文件的加载,以下也是,而关于类什么时候加载,并且类加载阶段为什么是线程安全的,可以去看一下jvm),也就是说在staitc变量上直接实例化,或者在静态代码块实例化再赋值。static变量在类加载时就以及存在类中了,以及静态代码块也会在类加载时进行执行。
public class SingletonObject{
//实现分为三步
//1.构造器私有化
private SingletonObject(){
}
//2.声明成员变量
private static final INSTANCE = new SingletonObject();
//如果是静态代码块的情况,与上面的第二步,二选一
private static final INSTANCE = null;
static{
INSTANCE = new SingletonObject();
}
//3.提供获取单例对象的方法
public static SingletonObject getInstance(){
return INSTANCE;
}
}
枚举
枚举声明的常量也是在枚举类加载阶段就进行实例化了
public Enum SingletonObject{
//有参数时如下,无参数时把括号去掉就好了
INSTANCE(1);
private final int value;
private SinletonObject(int value){
this.value = value;
}
}
懒汉式
懒汉式又分为线程安全和线程不安全
线程不安全
以下代码是在getInstance方法中进行实例化,判断类变量是否已赋值,没有赋值就实例化,赋值了则返回类变量。不过该用法是线程不安全的,原因是:假设两个线程同时进入了getInstance方法中的if语句判断INSTANCE为null,则两线程会接下去执行INSTANCE = new SingletonObject();语句,假设有一个线程比较快执行,然后返回实例给调用者,另一个线程还在实例化语句,等慢的线程实例化完成后返回,那么此时两线程执行该方法得到的返回值就会不一致。因此线程不安全,只适用于单线程环境。
public class SingletonObject{
//还是那三步
//只不过懒汉式在类变量上不立即实例化
private static final INSTANCE = null;
private SingletonObject(){}
public staitc SingletonObject getInstance(){
if(INSTANCE == null){
INSTANCE = new SingletonObject();
}
return INSTANCE;
}
}
线程安全
同步锁
本方法是使用synchronized修饰静态方法或者修饰该类的同步代码块,而synchronized方法修饰静态方法,在访问该方法时,先判断是否拿到该类的锁,没有的话阻塞,不让其他线程访问该类的该方法,直到方法执行完毕才释放锁。修饰同步代码块也是差不多的。与上一个代码区别也只是一个synchronized修饰符,该方法线程安全,只不过每次一进入这个方法就要将该方法锁住,不让其他线程访问,得等到锁释放,其他线程才能访问,这样做是线程安全了但是效率很低,还有没有改进方案呢?答案是:双重检查锁或静态内部类。
public class SingletonObject{
//还是那三步
//只不过懒汉式在类变量上不立即实例化
private static final INSTANCE = null;
private SingletonObject(){}
public staitc synchronized SingletonObject getInstance(){
if(INSTANCE == null){
INSTANCE = new SingletonObject();
}
return INSTANCE;
}
}
双重检查锁(DCK) 重点
双重检查锁顾名思义就是双重判断+锁,为什么要这样做呢?还是假设两个线程都进入了该方法,如果一上来就锁的话明显效率低,那我先判断这个类变量是否为空,不为空就返回,为空就进去实例化,这样想是否不会因为锁而降低了效率?接下来我们再进行实例化,如果两个线程执行了实例化,明显最终返回的肯定不同,那么我们就需要采取措施,让他们只能一个执行实例化,解决方案就是先锁住这个类,再判断是否类变量为空。如果两个线程都要这个同步代码块,先进先锁,后进的等待,先进去的那一个先进行实例化,实例化完毕后,从同步代码块中退出并释放了锁,执行返回,而另一个在等待的线程此时发现锁已经释放了,也进入了代码块,显然不能直接实例化吧,因为前一个线程已经进行实例化了,那么我们得进入之后得再判断一次是否类变量为空,不为空,直接返回实例。
在下面的代码中还有一个重要的东西,是在变量上修饰volatile,该修饰符是为了禁止该变量在执行指令时指令重排。该修饰符的作用,什么是指令重排,可以去多线程编程了解一下。
public class SingletonObject{
//还是那三步
//只不过懒汉式在类变量上不立即实例化
private static final volatile INSTANCE = null;
private SingletonObject(){}
public staitc SingletonObject getInstance(){
if(INSTANCE == null){
synchronized(SingletonObject.class){
if(INSTANCE == null){
INSTANCE = new SingletonObject();
}
}
}
return INSTANCE;
}
}
静态内部类
静态内部类单例模式是线程安全的,并且有懒加载的效果。因为该静态内部类只有在调用静态内部类时才加载,所以可以起到懒加载的作用,而加载过程是线程安全的。
public class SingletonObject{
//还是那三步
//只不过懒汉式在类变量上不立即实例化
private static final volatile INSTANCE = null;
private SingletonObject(){}
public staitc SingletonObject getInstance(){
return InnerSingletonObject.INSTANCE;
}
private static class InnerSingletonObject{
private final static SingletonObject INSTANCE = new SingletonObject();
}
}