[设计模式]单例模式
Posted baihan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[设计模式]单例模式相关的知识,希望对你有一定的参考价值。
[设计模式]单例模式
一、饿汉式
public class Hungry {
//浪费空间
private byte[] data1 = new byte[1024*1024];
private Hungry(){
}
private static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
饿汉式在类被初始化时就已经在内存中创建了对象,以空间换时间,故不存在线程安全问题。
二、懒汉式
public class Lazy {
private static Lazy lazy;//默认为null
private Lazy() {
}
public static Lazy getInstance() {
if (lazy == null) {
lazy = new Lazy();
}
return lazy;
}
懒汉式在方法被调用后才创建对象,以时间换空间,在多线程环境下存在风险。
三、DCL懒汉式(双重检测加锁)
public class Lazy {
private Lazy() {
}
private static Lazy lazy;
//DCL懒汉式(双重检测锁)
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
-
DCL模式的优点就是,只有在对象需要被使用时才创建。
-
第一次判断 INSTANCE == null避免非必要加锁的情况,这样可以提高执行效率。
-
使用synchronized锁住实例对象是保证程序在多线程环境下的安全性。
-
由于JVM存在乱序执行功能(指令重排),DCL也会出现在多线程场景下不安全的情况。例如:
Lazy lazy = new Lazy();
这一段代码不是原子性操作,在JVM里分为三步:- 在堆内存中开辟存储空间。
- 在堆内存中实例化Lazy里面的各个参数。
- 把对象指向堆内存空间。
解决办法:
private volatile static Lazy lazy;
加一个volatile关键字可以避免指令重排。
四、静态内部类模式
public class SingleTon{
private SingleTon(){}
private static class SingleTonHolder {
private final static SingleTon INSTANCE = new SingleTon();
}
public static SingleTon getInstance(){
return SingleTonHolder.INSTANCE;
}
}
-
外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不浪费内存。
-
即当SingleTon第一次被加载时,并不需要去加载SingleTonHolder,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE.
-
第一次调用getInstance()方法会使虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
-
静态内部类单例模式的核心原理为对于一个类,JVM在仅用一个类加载器加载它时,静态变量的赋值在全局只会执行一次。
-
静态内部类也有缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部不方便传递参数进去。
五、利用反射破坏单例模式
这里会用代码来展示反射是如何破坏DCL饿汉模式的。
-
第一场对决
DCL方:
public class Lazy { private Lazy() { } private volatile static Lazy lazy;//volatile避免指令重排 //DCL懒汉式(双重检测锁) public static Lazy getInstance() { if (lazy == null) { synchronized (Lazy.class) { if (lazy == null) { lazy = new Lazy();//不是原子性操作 } } } return lazy; } }
反射方:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException { Lazy lazy1 = Lazy.getInstance(); //反射开始操作 Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);//获取构造器 declaredConstructor.setAccessible(true);//无视私有构造器 Lazy lazy2 = declaredConstructor.newInstance();//通过构造器创建另一个实例 System.out.println(lazy1.hashCode());//输出实例对象的哈希值 System.out.println(lazy2.hashCode()); }
结果:出现了两个不同的实例对象,反射赢。
460141958 1163157884
-
第二场对决
DCL方:
public class Lazy { //在构造器加了锁和条件判断 private Lazy() { synchronized (Lazy.class){ if(lazy != null){ throw new RuntimeException("不要试图使用反射破坏单例模式!"); } } } private volatile static Lazy lazy;//volatile确保原子性,避免指令重排 //DCL懒汉式(双重检测锁) public static Lazy getInstance() { if (lazy == null) { synchronized (Lazy.class) { if (lazy == null) { lazy = new Lazy();//不是原子性操作 } } } return lazy; }
反射方:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true);//无视私有构造器 //两次都使用反射创建对象 Lazy lazy1 = declaredConstructor.newInstance(); Lazy lazy2 = declaredConstructor.newInstance(); System.out.println(lazy1.hashCode()); System.out.println(lazy2.hashCode()); } }
结果:出现了两个不同的实例对象,反射赢。
460141958 1163157884
-
第三场对决
DCL方:
public class Lazy { //加入标志位 private static boolean flag = false; private Lazy() { //进行标志位判断 if(flag == false){ flag = true; } else { throw new RuntimeException("不要试图使用反射破坏单例模式!"); } } private volatile static Lazy lazy;//volatile确保原子性,避免指令重排 //DCL懒汉式(双重检测锁) public static Lazy getInstance() { if (lazy == null) { synchronized (Lazy.class) { if (lazy == null) { lazy = new Lazy();//不是原子性操作 } } } return lazy; }
反射方:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException { Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true);//无视私有构造器 Lazy lazy1 = declaredConstructor.newInstance(); Field flag = Lazy.class.getDeclaredField("flag");//通过反射获取标志位字段 flag.setAccessible(true);//破坏字段私有性 flag.set(lazy1,false);//再将标志位改回false Lazy lazy2 = declaredConstructor.newInstance(); System.out.println(lazy1.hashCode()); System.out.println(lazy2.hashCode()); }
结果:出现了两个不同的实例对象,反射赢。
1163157884 1956725890
综上,反射完胜!
六、枚举实现单例模式
枚举方:
//枚举本身也是一个类
public enum Enum {
INSTANCE;
public Enum getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) {
Enum instance1 = Enum.INSTANCE;
Enum instance2 = Enum.INSTANCE;
//经检测,instance1和instance2是同一个对象。
System.out.println(instance1);
System.out.println(instance2);
}
}
以上就是使用枚举的方式实现单例模式,它的确可以保证一个类只有一个实例对象。我们现在唯一担心的就是反射。反射那么厉害,它可以破坏这种单例模式吗?让我们来试一下。
-
首选,我们先找到这个文件编译过后的字节码文件
Enum.class
。 -
然后利用反编译工具将此字节码文件反编译成java文件。
-
使用Idea自带的反编译工具。
-
使用命令行
javap -p Enum.class
。 -
使用第三方反编译工具jad的命令
jad -sjava Enum.class
。
-
-
jad反编译后的文件最准确,所以Enum类的构造器是
private Enum(String s, int i){super(s,i);}
。
反射方:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<Enum> declaredConstructor = Enum.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
Enum enum1 = declaredConstructor.newInstance();
System.out.println(enum1.hashCode());
Enum enum2 = declaredConstructor.newInstance();
System.out.println(enum2.hashCode());
}
结果:报错了,无法通过反射的方法破坏单例模式,枚举胜!
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
以上是关于[设计模式]单例模式的主要内容,如果未能解决你的问题,请参考以下文章