设计模式——单例模式
单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
饿汉式单例
由于饿汉式单例是在类加载的时候创建的实例,避免了线程安全问题,所以是线程安全的。
但是由于饿汉式是在类加载的时候就初始化,所以浪费内存。
/**
* Hungry 饿汉式单例
*/
public class Hungry {
//如果此时加入一个成员,那类加载的时候就初始化,会浪费内存
private Hungry() {
/*单例模式构造器都是私有的*/
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance() {
return HUNGRY;
}
}
静态内部类
package Design_Patterns.Single;
//静态内部类实现单例
public class Holder {
private Holder() {
//单例模式,都必须构造器私有
}
public static Holder getInstance() {
return InnerClass.HOLDER;
}
//一个静态的内部类
public static class InnerClass {
private static final Holder HOLDER = new Holder();
}
}
懒汉式单例
线程不安全
/**
* 懒汉式单例
*/
public class Lazy {
private Lazy() {
System.out.println(Thread.currentThread().getName() + "ok");
}
private static Lazy LAZY;
public static Lazy getInstance() {
if (LAZY == null) {
LAZY = new Lazy(); //不是一个原子性操作
}
return LAZY;
}
}
分析:假如在getInstance()方法中,判断LAZY为null后,CPU切换到另一个线程,再来判断又是null,CPU继续切换回刚开始那个线程,继续执行new对象操作,然后CPU切换回第二个线程,也会顺着继续执行new对象操作,此时的对象就不再是单个的对象,违反了单例模式。
我们对可以做一个测试:通过输出得知调用了四次构造函数,已经破坏了单例模式
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
Lazy.getInstance();
}
}.start();
}
}
/**output
Thread-1ok
Thread-3ok
Thread-0ok
Thread-2ok
*/
线程安全
为了解决线程安全问题,我们采用双重检验锁(DCL,即 double-checked locking)
/**
* 懒汉式单例
*/
public class Lazy {
private Lazy() {
synchronized(Lazy.class) {
if(LAZY != null) { //防止反射破坏
throw new RuntimeException("不要试图使用反射破坏单例");
} else {
System.out.println(Thread.currentThread().getName() + "ok");
}
}
}
private volatile static Lazy LAZY;
//双重检测锁模式的懒汉式单例
public static Lazy getInstance() {
if (LAZY == null) {
synchronized (Lazy.class) {
if (LAZY == null) {
LAZY = new Lazy(); //不是一个原子性操作
}
}
}
return LAZY;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
Lazy.getInstance();
}
}.start();
}
}
}
分析:
为什么给LAZY对象加volatile关键字
在Java中new一个对象并非一个原子操作,可分为三步:
- 分配内存空间
- 执行构造方法,初始化对象
- 把这个对象指向空间
由于new对象并不是一个原子操作,所以可能发生指令重排,执行顺序可能是123,也可能是132,假如指令执行顺序变成了132:
- 假如A进程刚进来,先分配内存空间,再把对象指向这个空间
- 此时进来一个线程B,由于LAZY已经指向了一个空间,它会认为对象不为null,所以会直接返回
- 此时LAZY还未完成构造,空间是一片虚无,所以LAZY必须要避免指令重排,加volatile
反射对单例的破坏
Java的反射可以从class中反射出构造函数,从而达到创建对象的目的,也就破坏了单例的“只有一个实例”。
public static void main(String[] args) throws Exception {
Lazy lazy1 = Lazy.getInstance();
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); //构造器是空参
declaredConstructor.setAccessible(true);
Lazy lazy2 = declaredConstructor.newInstance();
System.out.println(lazy1.hashCode() + " ::: " + lazy2.hashCode());
}
/** output
705927765 ::: 366712642
*/
可以看出两个对象并不是同一个对象,而是不同的两个对象,所以单例模式被破坏了。所以在构造函数里我们应该加上对对象的判断,如果LAZY已经不为空,就要抛出异常。
more try
当然除此之外,就算在构造器中加入了判断,也可以利用反射对单例造成破坏。判断是根据类中声明的对象是否为空来作为依据的,如果我们不调用getInstance()方法,而是直接利用反射构造出两个对象,即可避过这种检查,使LAZY一直等于null。
public static void main(String[] args) throws Exception {
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); //构造器是空参
declaredConstructor.setAccessible(true);
Lazy lazy1 = declaredConstructor.newInstance();
Lazy lazy2 = declaredConstructor.newInstance();
System.out.println(lazy1.hashCode() + " ::: " + lazy2.hashCode());
}
/**output
705927765 ::: 366712642
*/
出现了这种情况,我已经可以解决。加入一个变量,这个变量的名字可以是加密过后的,在构造器中继续加入判断
private static boolean flag = false; //表示还未调用过构造器new对象
private Lazy() {
synchronized(Lazy.class) {
if(flag == false) { //还未new过对象
flag = true;
} else {
throw new RuntimeException("不要尝试使用反射破坏单例");
}
}
}
当然,这种也不是绝对安全的,如果利用反编译技术,可以得到flag这个变量(虽说已经加过密,但有加密也就有解密),那么flag依旧可以被反射出来,看下面示例:
public static void main(String[] args) throws Exception {
//对flag变量的反射
Field flag = Lazy.class.getDeclaredField("flag");
flag.setAccessible(true);
//对构造器的反射
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); //构造器是空参
declaredConstructor.setAccessible(true);
Lazy lazy1 = declaredConstructor.newInstance();
flag.set(lazy1, false); //将标志变量又变回false
Lazy lazy2 = declaredConstructor.newInstance();
System.out.println(lazy1.hashCode() + " ::: " + lazy2.hashCode());
}
/** output
366712642 ::: 1829164700
*/
所以反射本就是一个bug,需要见招拆招,而不是一味的墨守成规。
枚举
JDK1.5开始引入了枚举类型,它可以防止反射来破坏单例。
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
import java.lang.reflect.Constructor;
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class Test {
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
/** output
Exception in thread "main" java.lang.NoSuchMethodException: EnumSingle.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at Test.main(EnumSingle.java:14)
*/
抛出的异常说明enum中根本没有一个空参的构造方法,通过将class反编译为java文件,发现我们的类继承了枚举类,而构造器并非空参构造器,而是有参构造器,一个String和一个int
//更改一下
class Test {
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1.hashCode());
System.out.println(instance2.hashCode());
}
}
/** output
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at Test.main(EnumSingle.java:16)
*/
这样说明反射确实无法破解枚举的单例
总结
实现单例模式有四种方式,饿汉式、懒汉式、静态内部类、枚举。
饿汉式:
- 线程安全
- 由于在类加载的时候初始化,浪费内存
懒汉式:
- 要想线程安全得加锁,但加锁就会影响效率,但getInstance方法由于调用机会不多,所以影响不是很大
- 第一次调用才初始化,避免内存的浪费。
静态内部类:
- 线程安全
枚举:
- 线程安全
- 绝对防止多次实例化
- 自动支持序列化