单例模式详解
饿汉式
/** * 饿汉模式(静态变量) */ public class HungryMan { //构造器私有化 (防止 new ) private HungryMan(){} //类的内部创建对象 private final static HungryMan hungryMan = new HungryMan(); //对外暴露一个静态的公共方法 public static HungryMan getInstance(){ return hungryMan; } } /** * 饿汉模式(静态代码块) */ public class HungryMan { //构造器私有化 (防止 new ) private Hungry(){} //类的内部创建对象 private final static HungryMan hungryMan; static{ hungryMan = new HungryMan(); } //对外暴露一个静态的公共方法 public static HungryMan getInstance(){ return hungryMan; } }
以上的两种方式其实差不多,有一样的优缺点:
在类加载时完成实例化,避免了线程同步问题。
如果从头到尾都没有用过该实例,就会造成内存的浪费。
总结:可以用,但可能会造成浪费,不推荐使用
懒汉式
/** * 懒汉式(线程不安全) */ public class LazyMan { private LazyMan(){} private static LazyMan instance; //提供一个静态的公有方法,当使用到该方法时,再去创建instance public static LazyMan getInstance(){ if(instance == null){ instance = new LazyMan(); } return instance; } }
虽然起到了懒加载的效果,避免内存浪费。但是会造成线程不安全。
如果在多线程下,一个线程进入了if判空,还未来得及往下new对象,另一线程也进入了If判空,就会产生多个实例。因此,在实际开发中不能这样使用单例模式。
所以我们就会想到要解决线程不安全的问题,在getInstnce()方法中加上 synchronized锁
public static synchronized Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance; }
但是这样会造成效率低下的问题,每个线程在想获得类的实例时,还得等其他线程执行完这个同步方法。实际上,这个方法只需要执行一次实例化代码就够了,后面的直接return。
有一种很优秀的DCL(双重检查锁)模式,不仅达到了懒加载效果,还保证了线程安全以及提高效率。
//双重检查代码(Double-Check) public class LazyMan { //单例模式必须构造器私有 private LazyMan(){} private static LazyMan lazyMan; //双重检测锁模式 懒汉式单例 DCL(双重检查锁)懒汉式 public static LazyMan getInstance(){ //第一层判空 提高性能 避免资源浪费 //如果不判空的话 就跟第三种的效率是一样低的 if ( lazyMan == null ) { //step1 synchronized (LazyMan.class) { //由于可能多个线程都进入了step1,由于锁定机制,一个线程进入该代码块时,其他线程 //仍在排队进入该代码块,如果不做判断,当前线程即使创造了实例,下一个线程也不知道,就会继续创建一个实例。 if (lazyMan == null) { lazyMan = new LazyMan(); //不是一个原子性操作 } } } return lazyMan; } //我们可以创建一个man方法进行测试 public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ LazyMan lazyMan = LazyMan.getInstance(); System.out.println(lazyMan.hashCode()); }).start(); } } }
结果显示:
看起来似乎没有什么问题了,但实际上存在一个并发陷阱---- 当lazyMan != null时,仍可能指向一个空对象
因为 lazyMan = new LazyMan(); //不是一个原子性操作,它可以被抽象为下面几条JVM指令
memory = allocate(); //1:分配对象的内存空间 initLazyMan(memory); //2:初始化对象 lazyMan = memory; //3:设置lazyMan指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序
操作 3很有可能 排在了操作 2 之前,即引用lazyMan指向内存memory时,这段崭新的内存还没有初始化——即,引用lazyMan指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于lazyMan已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。
这个时候,需要volatile来解决了! vlatile能保持内存可见性
//再静态变量前加上 volatile关键字 private static volatile LazyMan lazyMan;
静态内部类
//静态内部类 既达到了懒加载效果,又保证了线程安全 public class OuterClass { private OuterClass() { } public static class InnerClass { private static final OuterClass outerClass = new OuterClass(); } public static OuterClass getInstance(){ return InnerClass.outerClass; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ OuterClass outerClass = OuterClass.getInstance(); System.out.println(outerClass.hashCode()); }).start(); } } }
结果就不贴出来了,每个线程里的实例化对象哈希码都是一样,即同个实例。
当Holder类被加载时,静态内部类并不会马上被加载,只有调用Holder的getInstance()方法时,静态内部类才会被加载,这就实现了第一点:懒加载效果;
而类的静态属性只会在第一次加载类时初始化。 也就是说当JVM装载类时,底层提供了装载机制保证了初始化是单线程的,即线程安全的。
推荐使用
枚举
//枚举本身也是一个class public enum SingleTon { INSTANCE; } //创建一个测试类 public class TestSingleton { public static void main(String[] args) { SingleTon instance = SingleTon.INSTANCE; SingleTon instance2 = SingleTon.INSTANCE; System.out.println(instance == instance2); } }
结果输出:
反射会破坏单例模式
package single; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; //懒汉式单例 public class LazyMan { //设置一个标志位 private static boolean flag = false; private LazyMan(){ synchronized (LazyMan.class){ if ( flag == false ){ flag = true; }else{ throw new RuntimeException("不要试图使用反射破坏异常"); } } System.out.println(Thread.currentThread().getName()+"OK"); } private static LazyMan lazyMan; //双重检测锁模式 懒汉式单例 DCL懒汉式 public static LazyMan getInstance(){ //第一层判空 提高性能 避免资源浪费 //如果不判空的话 不管该对象是否已经创建 线程都要进入同步代码块 if ( lazyMan == null ) { //step1 synchronized (LazyMan.class) { //由于可能多个线程都进入了step1,由于锁定机制,一个线程进入该代码块时,其他线程 //仍在排队进入该代码块,如果不做判断,当前线程即使创造了实例,下一个线程也不知道,就会继续创建一个实例 if (lazyMan == null) { lazyMan = new LazyMan(); //不是一个原子性操作 } } } return lazyMan; } public static void main(String[] args) throws Exception { Field flag = LazyMan.class.getDeclaredField("flag"); flag.setAccessible(true); Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null); declaredConstructor.setAccessible(true); //跟LazyMan.getInstance(); 不是同一个实例 LazyMan instance = declaredConstructor.newInstance(); flag.set(instance,false); //当设置有标志位,两个通过反射生成的实例对象依旧有办法通过 并且不是同一个实例 //利用属性重新赋值 flag.set(instance,false); LazyMan instance2 = declaredConstructor.newInstance(); System.out.println(instance); System.out.println(instance2); } }
反射不能破坏枚举
package single; import java.lang.reflect.Constructor; import java.util.EmptyStackException; //enum 是一个什么? 本身也是一个class类 public enum EnumSingle { INSTANCE; public EnumSingle getInstance(){ return INSTANCE; } } class Test{ public static void main(String[] args) throws Exception { EnumSingle instance1 = EnumSingle.INSTANCE; //EnumSingle instance2 = EnumSingle.INSTANCE; Constructor<EnumSingle> declaredConstruct = EnumSingle.class.getDeclaredConstructor(String.class, int.class); declaredConstruct.setAccessible(true); EnumSingle instance2 = declaredConstruct.newInstance(); System.out.println(instance1); System.out.println(instance2); // java.lang.NoSuchMethodException: single.EnumSingle.<init>() 没有空参构造器 不是我们想要的错误 // 把无参换成有参 就会报我们理想的错误:Cannot reflectively create enum objects } }