单例模式(Singleton Pattern)是Java中最简单的设计模式之一,它属于创建型设计模式。单例模式设计到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,并且该类要提供一种访问这个唯一对象的方式。所以单例类有以下的几个规范:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
单例模式主要解决的问题是:当一个全局使用的类频繁地创建和销毁,其带来的系统损耗会很大。使用单例模式可以使系统中只有一个实例,只需创建一次,在需要的时候通过单例类提供的方法来调用这个实例。
单例模式的UML图
+ 表示被public修饰,- 表示被private修饰
单例模式的实现方式
单例模式的实现方式主要有这几种:
- 懒汉式
- 饿汉式
- 双重校验锁
- 静态内部类
我们按顺序来了解这几种方式。
懒汉式
懒汉式顾名思义就是等到需要用到这个实例时我再去实例化它。在懒汉式中,如果单例已经创建,则直接使用已经创建好的对象返回,否则创建一个新的对象返回。
public class Singleton2 {
private static Singleton2 instance = null;
private Singleton2(){
}
public static Singleton2 getInstance(){
if(instance == null)
instance = new Singleton2();
return instance;
}
}
上边时懒汉式最基本的实现方式,它最大的问题在于不支持多线程。因为当多线程调用getInstance()方法时,有多个线程同时进入if判断语句中,此时有可能instance正在实例化,但还没有完成实例化的过程,此时这几个线程都会去实例化这个类,使得失去了单例模式的效果。
当然,我们也可以通过加synchronized的方式来实现线程安全的懒汉式
public synchronized static Singleton2 getInstance(){
if(instance == null)
instance = new Singleton2();
return instance;
}
我们解决了多线程同步的问题,但是又会有新的问题产生,synchronized会导致效率下降。这个问题,我们待会在双重校验锁中解决他。
饿汉式
首先我们先明白什么叫饿汉式,饿汉模式就是无论你是否需要用到这个单例类,都会在内存中先去实例化这个类,这个实例化过程在类加载的过程中(后边JVM的类加载过程会讲)完成。就像一个饿汉,只要有这个类,我就要吃掉(实例化)它。因此所有在类加载过程中完成单例类的实例化的方式都是饿汉式。下边我们简单介绍一种饿汉式
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
private Singleton1(){
}
public static Singleton1 getInstance(){
return instance;
}
}
这种实现方式是最简单的一种实现方式,它是一种线程安全的创建方式,不需要加锁,执行效率会提高,但是因为它在类加载的过程中就初始化,如果在后续没有使用到这个单例类,就会造成内存浪费的问题。
这种实现方式适合单例占用内存比较小,在初始化时就会被用到,但是,如果单例占用的内存比较大,或者只在某特定的场景下才会用到,使用懒汉式会比饿汉式更适合。
双重校验锁
双重校验锁其实也是一种懒汉式,但它解决了懒汉式中直接在getInstance()方法上加synchronized所导致的效率问题。我们先来看看它的实现代码:
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3(){
}
public static Singleton3 getInstance(){
if(instance == null){
synchronized(Singleton3.class){
if(instance == null){
instance = new Singleton3();
}
}
}
return instance;
}
}
通过双重校验和synchronized,我们解决了线程安全问题和效率问题,但是问题总是源源不断地。
首先要先说下Java中地指令重排优化,所谓指令重排优化时指在不改变源语义地情况下,通过调整指令地执行顺序让程运行得更快。由于指令重排优化地存在,导致初始化Singleton和将对象地址赋给instance字段地顺序时不确定。
例如:
线程A在创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象设置为默认值。此时线程A就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有完成初始化操作。线程B来调用newInstance()方法,得到的就是未初始化完全的单例对象,这就会导致系统出现异常行为。
而在JDK1.5之后增加了volatile关键字,volatile地一个语义就是禁止指令重排序优化,也就能够解决我们上边所讲到地问题。因此我们只需要给instance变量加上volatile关键字即可。
public class Singleton3 {
private static volatile Singleton3 instance = null;
private Singleton3(){
}
public static Singleton3 getInstance(){
if(instance == null){
synchronized(Singleton3.class){
if(instance == null){
instance = new Singleton3();
}
}
}
return instance;
}
}
静态内部类
静态内部类也是一种懒汉式,能达到双重校验锁一样的功效,且实现更加简单。这种方式只适用于静态域的情况,而双重校验锁可以在实例域需要延迟初始化时使用。
public class Singleton4 {
private static class SingletonHolder{
private static final Singleton4 instance = new Singleton4();
}
private Singleton4(){
}
public static final Singleton4 getInstance(){
return SingletonHolder.instance;
}
}
这种方式同样利用了了加载机制来保证线程安全问题,即只会有一个实例。它与双重校验锁不同的地方在于,双重校验锁只要单例类被装载了,那么instance就会被实例化(并没有达到延迟加载的效果),而这种方式被装载时不一定会实例化,因为SingletonHolder没有被主动使用,只有调用了getInstance()才会显示装载Singleton Holder,从而实例化instance。
当实例化instance很消耗资源,想要延迟加载instance,又不希望Singleton加载时就实例化,此时这种方式就会比双重校验锁更加合适。
参考:https://blog.csdn.net/qq_24047659/article/details/86747024