前言
之前研究了创建型设计模式中的工厂模式,这篇文章学习一下创建型设计模式中的单例模式
一、单例模式
顾名思义,单例模式就是在程序运行过程中,全局只需要一个实例时,就可以使用单例模式,既然单例模式要保证全局唯一,则构造函数必须私有,变量被static修饰。这样才可以保证其它类无法实例化该此类,必须通过对应的方法获取唯一示例。单例模式的优点是可以避免对象的重复创建,节省资源。单例模式从创建的时机可以分为饿汉式、懒汉式和静态内部类三种。
饿汉式
饿汉式,即变量被声明时即被创建,代码示例如下:
public class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton(){
};
public static HungrySingleton getInstance(){
return instance;
}
}
从上面代码可以看出,变量在被声明即被创建,饿汉式的优点是线程安全的,但是饿汉式的弊端就是,即使这个单例无论使不使用,在类加载之后也会被立马创建,占用内存并增加类的初始化时间。
懒汉式
懒汉式相比饿汉式,区别是,变量声明时并不进行加载,而是实际使用的时候才加载,有效的避免了饿汉式的缺点,最简的饿汉式代码示例如下:
public class UnsafeLazySingleton {
private static UnsafeLazySingleton instance;
private UnsafeLazySingleton(){
}
public static UnsafeLazySingleton getInstance(){
if(instance == null) { // 1
instance = new UnsafeLazySingleton();
}
return instance;
}
}
上面的代码示例虽然解决了饿汉式的问题,但是是线程不全的,比如现在有线程1和线程2同时执行到step,此时两个线程都会判断成功并进入实例的创建,因此,在多线程的场景下,可能存在示例重复创建的过程,因此要给getInstance方法加锁来保证线程的同步,最常见的懒汉式单例代码如下:
public class LazySingleton {
private volatile static LazySingleton instance;
private LazySingleton(){
}
public static LazySingleton getInstance(){
if(instance == null) { // 1
synchronized (LazySingleton.class){
if(instance == null){
instance = new LazySingleton();//2
}
}
}
return instance;
}
}
这里有两个问题:
-
在给类加锁前为什么要先判断instance是否为null。
sychronized关键字锁住Singleton类而不是instance对象,是因为加锁时如果nstance为空,加sychronized没有任何意义。
第一重判断的意义在于:如果instance非空,就跳过了锁的步骤,减少加锁的资源开销。但是由于第一重判断在代码锁之外,如果不同线程同时访问到instance==null,会先后阻塞执行代码锁内的内容。所以在代码锁内加第二重判断就很有必要了,避免第一个线程获取实例后,第二个线程获得资源锁又执行了一次instance的初始化,产生两个不同的实例。 -
变量前为什么被volatile关键字修饰。
volatile关键字的一个重要作用是防止指令重排,代码中2的赋值语句并不是一个原子操作(一条指令即可完成),而是可以拆分成以下过程:-
分配内存空间。
-
初始化对象。
-
将对象指向刚分配的内存空间。
因为步骤2和步骤3之间并没有数据依赖关系,所以JVM可以对指令进行重排序,也就是可以先进行步骤3后进行步骤2。这会导致另一个线程在获取变量时就会将代码中位置1的条件判断为false,直接返回instance的引用,实际返回的是一个并未初始化完全的实例,会带来线程上的安全问题。
-
静态内部类
除了饿汉式和懒汉式两种单例方式,还存在另一种单例,静态内部类的方式,代码示例如下:
public class InnerClassSingleton {
private static class InnerClassSingletonHolder {
public static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton(){
}
public InnerClassSingleton getInstance(){
return InnerClassSingletonHolder.instance;
}
}
静态内部类的单例模式有两个优点,第一是可以实现懒加载,即懒汉式的优点,第二是可以保证线程安全,下面针对这两点做说明。
- 实现懒加载
Java类加载的过程包括:加载、验证、准备、解析、初始化。初始化阶段即执行类的 clinit 方法(clinit = class + initialize),包括为类的静态变量赋初始值和执行静态代码块中的内容。但不会立即加载内部类,内部类会在使用时才加载。所以当此 Singleton 类加载时,SingletonHolder 并不会被立即加载,所以不会像饿汉式那样占用内存。
另外,Java 虚拟机规定,当访问一个类的静态字段时,如果该类尚未初始化,则立即初始化此类。当调用Singleton 的 getInstance 方法时,由于其使用了 SingletonHolder 的静态变量 instance,所以这时才会去初始化 SingletonHolder,在 SingletonHolder 中 new 出 Singleton 对象。这就实现了懒加载。 - 线程安全
Java 虚拟机的设计是非常稳定的,早已经考虑到了多线程并发执行的情况。虚拟机在加载类的 clinit 方法时,会保证 clinit 在多线程中被正确的加锁、同步。即使有多个线程同时去初始化一个类,一次也只有一个线程可以执行 clinit 方法,其他线程都需要阻塞等待,从而保证了线程安全。
总结
如有错误,恳请大家批评指正,日拱一卒,功不唐捐。
参考:
https://www.cnblogs.com/shan1393/p/8999683.html
https://zhuanlan.zhihu.com/p/85624457