单例模式算是最基础的一种设计模式,但是在工作中应用很广泛。单例模式的实现方式有很多种,大体上呈现出推进演变的趋势,大家常用的分类方式是将单例模式分为饿汉式和懒汉式,本文也将沿着单例模式演变的脉络介绍单例模式的实现方式。
首先学习一个东西需要了解这个技术是干什么的,解决了哪些问题。在开发中很多类的对象是不需要new
多个对象,比如配置文件的对象存储了许多配置参数,并且通常这些配置参数是不变的,那么如果将该对象设计成唯一的就将复杂的配置简单化了,单例模式就是保证该类的对象只有一个的方法。
饿汉式
单例模式中饿汉模式。 饿汉模式中的类实例在类被加载时就被初始化出来了,因此在应用初始化的时会占用不必要的内存。同时,由于该实例在类被夹在的时候就创建出来了,所以它是线程安全的,因为类初始化是由ClassLoader
完成的,而Classloader
的loadClass
方法在加载类的时候使用了synchronized
关键字实现线程同步。
饿汉模式的代码示例如下图所示:
public class Singleton_01 {
private static Singleton_01 instance = new Singleton_01();
private Singleton_01(){}
public static Singleton_01 getInstance(){
return instance;
}
public static void main(String[] args) {
}
}
在实际使用中,这种实现方式是被推荐的,因为简单!!!而且是线程安全!!!的,但是它也存在一些问题。比如这个类不管在什么情况下都是实例化一个Singleton_01
对象,那么假如这个类不被使用,那实例化这个对象的内存就被浪费了。那当然不行了!强迫症不能忍!我们试着对这个实现方式进行优化,于是就有了懒汉式实现单例方式。
懒汉式
既然我们不想在类加载时候就实例化类对象,那我们就把它改一下,如下实现:
public class Singleton {
private static Singleton INSTANCE = null;
public static Singleton getInstance(){
if (INSTANCE == null){
new Singleton();
}
return INSTANCE;
}
}
乍一看好像没啥问题,但是我们考虑一下多线程环境下,假设线程一
执行了到了INSTANCE==null
这条语句判断为空但是还没有执行到new Singleton()
这条语句,这时候线程二
刚好也执行到了INSTANCE==null
这条语句,那么它也会判断为空,这样就实例化了两个对象。因此这种实现方式是线程不安全的,好的那么我们都知道synchronized
是可以锁住对象保证线程安全的,我们将上面的代码改成如下所示:
public class Singleton {
private static Singleton INSTANCE = null;
public static synchronized Singleton getInstance(){
if (INSTANCE == null){
new Singleton();
}
return INSTANCE;
}
}
这样的实现方式当然是线程安全的,但是直接锁住一个方法,是否会对性能造成影响呢?答案是肯定的,在多线程环境下每个线程想要获取Singleton
对象都需要去获得这把锁,而这个方法中也许会有一些业务代码是不需要上锁的,这样粗暴的上锁方式是不合适的并且代码效率也不高,那我们当然不能忍!于是就有了下面的解决方案,细化锁,我们只对实例化操作上锁:
public class Singleton {
private static Singleton INSTANCE = null;
public static Singleton getInstance(){
//业务代码
if (INSTANCE == null) {
synchronized (Singleton.class) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
但是不幸的是,和上面一样,这种实现方式也是线程不安全的。因为在一个线程获取锁时,另一个线程可能也进入了这个判断,在等待前一个线程释放锁,这样也可能发生实例化对象不唯一的错误。可能这时候你会有疑惑,为什么不在if
条件判断外面上锁,这个问题很好想明白,如果这样做的话任然是每次获取这个INSTANCE
都需要去获取锁。为了解决上面这种实现方式的问题,双重检查的懒汉式单例实现就出来了,代码如下所示:
public class Singleton {
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
这样即使另外一个线程等待锁的释放由于内部还有一重检查,也不会创建第二个实例对象,保证了线程安全。
需要注意的是,INSTANCE
的volatile
关键字不能少。原因是这样禁止了实例化Singleton
对象时发生指令重排序,在JVM
中实例化对象并不是一个原子操作,而是分为三步:开辟空间、成员变量赋值、内存空间指向该对象的引用。在java
代码编译过程中为了提高效率,会对执行顺序进行优化,也就是指令重排序,我们想象一下这样的场景:先开辟空间再将内存空间地址指向该对象的引用,最后为成员变量赋值,那么这个时候另一个线程执行到if(INSTANCE == null)
这时该线程会判断当前INSTANCE
对象不为空,将直接返回INSTANCE
,但是这时候该对象并未进行赋值操作,这会使得我们得到了一个错误的数据,同样是线程不安全的。因此volatile
关键字是需要加上的。