简介
单例模式,顾名思义是一个类只有一个实例,在应用中,一般是通过getInstance()方法来获取其实例。此设计模式的好处是不需要大量创建不必要的实例,节省系统开销。该模式是属于创建型模式,一般有懒汉式、饿汉式、静态内部类和枚举等几种实现方式,以下就以代码的方式来说明各种方式的demo。
饿汉式
饿汉式,其明显的特征是在该类初始化的时候,就实例化了其单例对象,此方式实现最为简单,且能满足大部分的需求。其代码示例如下:
public class Demo1 {
private static final Demo1 INSTACE = new Demo1();
private Demo1() {}
public static Demo1 getInstace() {
return INSTACE;
}
}
饿汉式,其实现虽然简单,但在类加载的时候就去实例化对象,这对于一些性能要求较高的系统会有瑕疵,因此就出现了懒汉式。
懒汉式
懒汉式,又称DCL(Double Check Lock)实现方式。相对于饿汉式,其在类加载的时候不会去实例化对象,而是在需要的时候再去实例化。其实现相对于饿汉式要复杂些,需要注意的细节很多,下面首先给出实现代码示例,然后再分析其中的细节问题。
public class Demo2 {
private static volatile Demo2 INSTANCE;
private Demo2() {}
public static Demo2 getInstance() {
if (null == INSTANCE) {
synchronized (Demo2.class) {
if (null == INSTANCE) {
INSTANCE = new Demo2();
}
}
}
return INSTANCE;
}
}
需要注意的细节:
- 在申明对象的实例时需要加上volatile关键字,禁止在创建对象的时候进行指令重排序(volatile还有一个作用是保证可见性,但并不能保证原子性)。
- 在getInstance()时,一定要做两次非null判断,这样做的目的是防止多线程时创建多个实例。
静态内部类
使用静态内部类实现就是在类中声明一个静态内部类,然后在类中声明单例对象,最后在getInstance()方法中返回单例对象,以下是代码实例:
public class Demo3 {
private Demo3() {}
public static Demo3 getInstance() {
return INNER.INSTANCE;
}
static class INNER {
private static final Demo3 INSTANCE = new Demo3();
}
}
枚举实现
该方式是《Effective Java》书中推荐的方式,其实现也比较简单,示例代码如下:
public enum Demo4 {
INSTANCE;
public String doSomthing() {
return "hello world";
}
}
测试
各种单例模式的测试如下:
public class Test {
public static void main(String[] args) {
Demo1 d11 = Demo1.getInstace();
Demo1 d12 = Demo1.getInstace();
System.out.println(d11 == d12); // true
//-------------------------------
Demo2 d21 = Demo2.getInstance();
Demo2 d22 = Demo2.getInstance();
System.out.println(d21 == d22); // true
//-------------------------------
Demo3 d31 = Demo3.getInstance();
Demo3 d32 = Demo3.getInstance();
System.out.println(d31 == d32); // true
//-------------------------------
Demo4 d41 = Demo4.INSTANCE;
Demo4 d42 = Demo4.INSTANCE;
System.out.println(d41 == d42); // true
System.out.println(Demo4.INSTANCE.doSomthing()); // "hello world"
}
}
单例模式深扒
单例模式安全吗
对于饿汉式、懒汉式和静态内部类,当前的实现方式有风险吗?答案是肯定的,以饿汉式为例:
import java.lang.reflect.Constructor;
public class Demo1 {
private static final Demo1 INSTACE = new Demo1();
private Demo1() {}
public static Demo1 getInstace() {
return INSTACE;
}
public static void main(String[] args) throws Exception {
Demo1 instance = Demo1.getInstace();
Constructor<Demo1> constructor = Demo1.class.getDeclaredConstructor();
constructor.setAccessible(true);
Demo1 newInstance = constructor.newInstance();
System.out.println(instance.hashCode()); //356573597
System.out.println(newInstance.hashCode()); //1735600054
}
}
可以明显看出,通过反射新建的实例并不等于最开始的实例,其破坏了单例。我们可以在私有构造函数中做一个判断来解决此问题,代码如下:
import java.lang.reflect.Constructor;
public class Demo1 {
private static final Demo1 INSTACE = new Demo1();
private Demo1() {
synchronized (Demo1.class) {
if (null != INSTACE) {
throw new RuntimeException("singleton exception");
}
}
}
public static Demo1 getInstace() {
return INSTACE;
}
public static void main(String[] args) throws Exception {
Demo1 instance = Demo1.getInstace();
Constructor<Demo1> constructor = Demo1.class.getDeclaredConstructor();
constructor.setAccessible(true);
Demo1 newInstance = constructor.newInstance();
System.out.println(instance.hashCode()); //356573597
System.out.println(newInstance.hashCode()); //1735600054
}
}
懒汉式的双重检测中volatile
volatile的作用是内存可见性、禁止指令重排序,但不能保证原子性,如果不加volatile,那么在多线程环境下,执行new Demo2()会有指令重排的风险。 对于new Demo2(),该操作并不是一个原子操作,会有如下三个步骤: 1)分配内存空间; 2)执行构造方法,初始化对象; 3)把对象指向这个空间。 当执行到第3)时,instace就不为null。如果不禁止指令重排序,假设有T1和T2两个线程,T1线程执行new Demo2()时,执行的顺序不是1-2-3,而是1-3-2,当执行到第3步时,此时instance已经不为null,此时还没有执行2)而T2线程进来了,T2在第一个非null判断时发现instance已经非null就直接返回instance,然而此时的instance还没有实例化完成,因此就出现了问题,所以必须加volatile。