单例模式(Singleton)
指一个类只有一个实例,且该类能自行创建这个实例的一种模式。单例模式有如下特点:
1、单例类只有一个实例对象;
2、单例类必须自己创建自己的唯一实例。
3、单例类必须对外提供一个访问该单例的全局访问点。
对于全局统一,频繁使用的对象,为了避免频繁的创建和销毁这个对象,可以考虑使用单例模式,从而实现全局只有一个对象实例,节约系统资源。
单例模式实现的基本思路是:将构造函数变为私有;系统运行时,有且只有一次执行了new方法来创建单例对象,并提供一个方法用于对外获取这个对象。对于如何创建单例对象,一般有饿汉式和懒汉式两种。
饿汉式
饿汉式,不管使不使用这个单例对象,都会进行对象的创建。由于对象是提前创建的,所以饿汉式是线程安全的。
1、类加载时进行创建
public class Singleton1 {
private Singleton1() {}
private static Singleton1 instance = new Singleton1();
public static Singleton1 getInstance(){
return instance;
}
}
2、与1相同,都是在类加载时进行创建,只不过创建对象的代码放在了静态代码块,适用于需要多行代码进行创建对象的情况
public class Singleton2 {
private Singleton2() {}
private static Singleton2 instance;
static {
//do something
instance = new Singleton2();
}
public static Singleton2 getInstance(){
return instance;
}
}
懒汉式
懒汉式,顾名思义,对象会在使用时才会进行创建,首次使用时会受对象的创建影响可能比较慢,在多线程环境下需要额外的代码来处理线程安全的问题。
3、通过一个简单的null判断来创建对象,在多线程同时调用的情况下,可能会重复创建对象,线程不安全,不推荐使用
public class Singleton3 {
private Singleton3() {}
private static Singleton3 instance;
public static Singleton3 getInstance(){
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
4、针对3线程不安全的问题,可以通过对方法增加synchronized同步锁的办法来实现线程安全,不过当单例对象完成初始化之后,后续的每一次方法调用一样需要synchronized同步锁,效率比较低,不推荐使用
public class Singleton4 {
private Singleton4() {}
private static Singleton4 instance;
public static synchronized Singleton4 getInstance(){
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
5、双重检查锁,既然4中是由于synchronized锁的是整个方法导致的效率比较低,那我们可以通过双重检查的方式来改造代码,实现只有在创建单例时才进行加锁的操作
public class Singleton5 {
private Singleton5() {}
private static Singleton5 instance;
public static Singleton5 getInstance(){
if (instance == null) {
synchronized (Singleton5.class) {
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
}
第一个if判断很好理解,为null时需要进行单例的创建,非null时直接返回单例对象。在第一个if里面,通过synchronized对创建对象的操作进行加锁,由于第一个if在多线程下可能会有n个线程同时判断都为null,同时进入方法块。所以需要在synchronized里面再一次进行instance的null判断,这也就是为啥需要第二个if判断的原因。
双重检查锁的形式看起来完美的解决了线程不安全及效率低下的问题,但是他在某些情况下还是有问题的:
由于instance = new Singleton5();
不是一个原子性的操作,他可以分为三个部分:
//1.分配对象的内存空间
memory = allocate();
//2.初始化对象
ctorInstance();
//3.设置instance指向分配的内存空间
instance = memory;
正常情况下不会有问题,但是在某些情况中(在一些JIT编译器,我没实际验证复现过),代码的执行顺序会被改变,即指令重排。代码会被修改为1-3-2,而在这种情况下,instance执行完3之后已经不为null了,但是此时instance实际指向的对象还未完成初始化,也就是对象还不可用。此时另一个线程在第一个if判断中就会将还未完成初始化的instance直接返回供代码使用,而这里就出现了问题。
6、双重检查锁+volatile(推荐使用)
针对5的问题,我们可以通过把instance声明为volatile型来解决,因为volatile可以禁止指令重排,故而也就没有了5中的问题
public class Singleton6 {
private Singleton6() {}
private static volatile Singleton6 instance;
public static Singleton6 getInstance(){
if (instance == null) {
synchronized (Singleton6.class) {
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
}
7、静态内部类,由于静态内部类SingletonGet在Singleton7加载时并不会被加载,而是在第一次getInstance访问SingletonGet类时才会被JVM装载,通过JVM装载SingletonGet类时实现对单例的实例化创建,保证了线程的安全性,可以延迟加载,效率也高(推荐使用)
public class Singleton7 {
private Singleton7() {}
private static class SingletonGet{
private static final Singleton7 instance = new Singleton7();
}
public static Singleton7 getInstance(){
return SingletonGet.instance;
}
}
8、枚举形式,由于枚举的特性,通过JVM保证单例对象只初始化一次,实现线程安全。但是看起来比较怪,且代码比较繁琐,好像没什么人使用
public class Singleton8 {
private Singleton8() {}
private enum SingletonGet{
INSTANCE;
private Singleton8 instance;
SingletonGet() {
instance = new Singleton8();
}
public Singleton8 getInstance() {
return instance;
}
}
public static Singleton8 getInstance(){
return SingletonGet.INSTANCE.getInstance();
}
}