单例模式非常常见,某个对象全局只需要一个实例时,就可以使用单例模式,它的优点显而易见:
-
它能够避免对象重复创建,节约空间并提升效率
-
避免由于操作不同实例导致的逻辑错误
单例模式有两种实现方式:懒汉模式和饿汉模式
饿汉模式:变量在声明便初始化
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
可以看到,我们将构造方法定义为private,这就保证了其他类无法实例化类,必须通过getInstance()方法才能获取到唯一的instance实例,非常直观,但饿汉式有一个弊端,就是即使这个单例不需要使用,它也会在类加载之后立即创建出来,占用内存,并增加类初始化时间,就好比一个电工在修理灯泡时,先把所有工具拿出来,不管是不是所有工具都使用得到。
懒汉模式:先声明一个变量,需要时才初始化
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
我们先声明了一个初始值为null的instance变量,当需要使用时判断此变量是否已被初始化,没有初始化的话才new一个实例出来,就好比电工在修理灯泡,开始比较偷懒,什么工具都不拿,当发现需要使用螺丝刀时,才把螺丝刀拿出来,当需要使用钳子时,再把钳子拿出来
懒汉式解决了饿汉式的弊端,好处是按需加载,避免了内存浪费,减少了类初始化时间
上述代码的懒汉模式其实不是线程安全的,如果多个线程调用getInstance方法,变量就会被实例多次,为了保证线程安全,我们需要在代码判空过程中加上锁
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
}
这样就能保证在多个线程调用getInstance时,一次最多只有一个线程能够执行判空并new出实例的操作,所有instance只会实例化一次,但这样的写法仍然有问题,当多个线程调用getInstance时,每次都需要执行synchronized同步化方法,这样会严重影响程序的执行效率,所以更好的做法是在同步化之前,再加上一层检查:
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
这样增加一种检查方式后,如果instance已经被实例化,则不会执行同步化操作,大大提升了程序效率,上面这种写法也就是我们平时较常用的双检锁方式实现的线程安全的单例模式
但这样的懒汉模式仍然有一个问题,jvm底层为了优化程序运行效率,可能会对我们的代码进行指令重排序,在一些特殊情况下会导致出现空指针异常,为了防止这个问题,更进一步的优化是给instance变量加上volatile关键字
有一个问题,我们在外面检查instance == null,那么锁里面的空检查可以去掉吗
是不可以的,如果里面不做空检查,可能会有两个线程同时通过了外面的空检查,然后在一个线程new出实例后,第二个线程进入锁中又new出来一个实例
还有一种静态内部类方式保证懒汉模式的线程安全
public class Singleton {
private static class SingletonHolder {
public static Singleton instance = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
虽然我们经常使用这种静态内部类的加载方式,但其中的原理不一定清楚
- 静态内部类方式是怎么实现懒加载的
- 静态内部类方式是怎么保证线程安全的
java类的加载过程包括:加载,验证,准备,解析,初始化,初始化阶段即执行类的clinit方法,包括为类的静态变量赋初始值和执行静态代码块中的内容,但不会立即加载内部类,内部类会在使用过程中时才加载,所以当此Singleton类加载时,SingletonHolder并不会被立即加载,所以不会像饿汉式那样占用内存。
另外,Java虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类,当调用Singleton的getInstance方法时,由于其使用了SingletonHolder的静态变量instance,所以这时才会去初始化SingletonHolder,在SingletonHolder中new出Singleton对象,这就实现了懒加载。
第二个问题的答案就是,Java虚拟机的设计是非常稳定的,早已经考虑了多线程并发执行的情况,虚拟机在加载类的clinit方法时,会保证clinit在多线程中被正确的加锁,同步。即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行clinit方法,其他线程都需要阻塞等待,从而保证了线程安全