单例模式(Singleton Pattern)属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
通常来说:new实例化会创建一个对象,new多个就会创建多个实例化对象。而有时候若是想不管new多少都只想创建一个实例化对象并使用,就可以使用单例模式创建对象。
单例模式分为饿汉式和懒汉式。
饿汉式
饿汉式,形象的说法是饿的发慌,直接上来就开干。即类加载时就创建对象。
class Singleton{
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getSingleton(){
return singleton;
}
}
通过上述代码可以看到:饿汉式单例模式使用private声明了构造方法,这样其他类就不能直接通过构造方法实例化该对象,而饿汉式单例模式通过暴露的get方法提供了一种访问其唯一的对象的方式。
饿汉式类加载时就创建了实例,这样可能会造成内存浪费的问题。可以使用登记式/静态内部类的方式实现Lazy Loading,这种方式只适用于静态域的情况。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方式利用了 类加载机制来保证初始化 instance 时只有一个线程,这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式就比较适合。
ps:如果静态属性/方法有 final 修饰时,则不会加载
懒汉式
懒汉式顾名思义,太懒了,与饿汉式相反,等到需要使用的时候才创建对象。
class Singleton{
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
通过上述代码可以看到:懒汉式单例模式也使用private声明了构造方法,通过暴露的get方法提供了一种访问其唯一的对象的方式。不过懒汉式在get方法中多了一层判断,如果还没实例化那就实例化一个,若是已经实例化了,就返回该实例化对象。
饿汉式类加载时就创建了实例,这样可能会造成内存浪费的问题,而懒汉式虽然解决了这个问题,但是可能会造成线程不同步的问题。
如何解决呢?我们可以使用双检锁(DCL,double-checked locking),即:双重检验
class Singleton{
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
synchronized也可以加在get方法上,但是这样会十分影响性能,一般不会这么用。
如上述代码所示,DCL通过两重if判断解决线程不同步的问题,若是线程1,线程2都通过第一重检验 ,但只有第一个线程能拿到同步块的锁,当线程1拿到之后,线程2便被阻塞,直到线程1创建完成后释放锁。而后线程2拿到锁后,执行第二层检验,发现已经有实例后,无法通过第二层if判断。
双检锁第一层检验是为了完成实例化之后,之后的线程再不需要再执行synchronized代码块,提到效率;第二层检验是防止多次实例化。
执行instance = new Singleton();时会执行一下三个步骤:
- 在堆中开辟空间,分配内存
- 根据类加载的初始化顺序进行初始化
- 将内存地址返回给栈中的引用变量
由于Java内存可能因为性能原因进行重排序从而导致问题。此时可以用到volatile关键字防止重排序。若是不使用volatile,由于Java内存模型允许无序写入,有些编译器因为性能原因可能会将上述的顺序2和3调换,从而出现instance已经指向了指定的内存空间但还未初始化,而其他的线程访问instance却还未初始化的问题。添加volatile关键字就相当于在其中加了一层内存屏障。
ps:JDK5 以及后续版本扩展了volatile语义,不再允许volatile写操作与其前面的读写操作重排序,也不允许volatile读操作与其后面的读写操作重排序