单例模式.
目录
♫什么是单例模式
♫饿汉式单例模式
♫懒汉式单例模式
♫单例模式的线程安全问题
♪原子性
♪内存可见性与指令重排序
♫什么是单例模式
单例模式是一种设计模式,通过巧用Java的现有语法,实现一个只能被创建一个实例的类,并提供一个全局访问点。在有些创场景中,一些特点的类只能创建一个实例,虽然不依赖单例模式我们也可以控制类的实现个数,但通过单例模式实现的类就相当于有了语法约束,即使想要实现多个实例都很难。在Java中,单例模式的实现有多种方式,下面是两种比较常用的实现方式。
♫饿汉式单例模式
饿汉式单例模式是指在类加载时就创建实例对象,通过static关键字保证在程序的整个生命周期中只存在一个实例对象:
public class Singleton { // 静态成员变量,用来记录唯一实例 private static Singleton uniqueInstance = new Singleton(); // 私有构造方法,防止外部通过new关键字创建实例 private Singleton() {} // 静态工厂方法,返回唯一实例 public static Singleton getInstance() { return uniqueInstance; } }
通过将构造方法设置为私有的,保证类外无法通过new来创建实例的同时,通过static将uniqueInstance成员属性修饰为类属性(Java代码中的每个类在编译完成后都会生成.class文件,JVM加载时通过读取.class文件中的二进制指令来在内存中构造出类对象(Singleton.class),类对象的属性就是类属性),由于类对象只有一份,故类属性也就只有一份。
♫懒汉式单例模式
懒汉式单例模式是指在第一次访问时才创建实例对象,在第一次访问之前则不创建对象:
public class Singleton { // 静态成员变量,用来记录唯一实例 private static Singleton uniqueInstance = null; // 私有构造方法,防止外部通过new关键字创建实例 private Singleton() {} // 静态工厂方法,返回唯一实例 public static synchronized Singleton getInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
懒汉模式与饿汉模式的区别在于懒汉模式只有在第一次使用时才创建实例对象,不使用则不创建实例对象,而饿汉模式则不管有没有使用都会在使用前(类加载时)创建一个实例对象。
♫单例模式的线程安全问题
在多线程环境下,饿汉模式只有到读操作不需要考虑线程安全问题,而懒汉模式既有读又有写,这就得涉及到线程安全了。
1.在if语句中可以分为读、比较、写三步,由于这三步骤不是原子性的,在多线程环境下就可能发生第一个线程读完还未写入前,第二个线程也开始读,从而导致可能多次执行new操作。
♪原子性
我们可以通过synchronized来保证读、比较、写的原子性:
//懒汉模式 class Singleton2 { private static Singleton2 uniqueInstance = null; public Singleton2 getInstance() { synchronized (Singleton2.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton2(); } } return uniqueInstance; } private Singleton2() {} }
虽然通过给类对象加锁保证了if语句的原子性,但这样写每一次使用都需要进行加锁操作加大了开销,故我们还可以在加锁前再判断下需不需要进行加锁操作:
//懒汉模式 class Singleton2 { private static Singleton2 uniqueInstance = null; public Singleton2 getInstance() { if (uniqueInstance == null) { synchronized (Singleton2.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton2(); } } } return uniqueInstance; } private Singleton2() {} }
注:第一个if判断是否需要加锁,第二个if判断是否需要创建对象
♪内存可见性与指令重排序
懒汉模式不仅有原子性问题还有内存可见性与指令重排序的隐患:
♩内存可见性:在多线程环境下,有多个线程同时进行getInstance操作,有可能编译器只有第一次是从内存中读取uniqueInstance的值,其它时候都是直接从寄存器或cache中读取uniqueInstance的值,也会导致多次创建实例对象。
♩指令重排序:uniqueInstance=new Singleton2()操作可以拆分为三步:①.申请内存空间②.调用构造方法在内存空间上创建一个实例对象③.把内存空间的值赋值给uniqueInstance。正常情况下顺序执行①②③是没有问题的,但无法保证编译器不会优化这三步骤的执行顺序,若是执行顺序为①③②,那么就可能在一个线程执行到②之前就调度去另一个线程执行①,这就会导致内存空间后面被初始化,而该线程就以为对象创建好了,如果这时候这个线程使用对象的属性方法就会出现问题。
解决内存可见性与指令重排序就需要给uniqueInstance加上volatile关键字:
//懒汉模式 class Singleton2 { private static volatile Singleton2 uniqueInstance = null; public Singleton2 getInstance() { if (uniqueInstance == null) { synchronized (Singleton2.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton2(); } } } return uniqueInstance; } private Singleton2() {} }