面经-单例模式
单例模式的五种实现方式
1.饿汉式
public class Singleton1 implements Serializable { private Singleton1() { if (INSTANCE != null) { throw new RuntimeException("单例对象不能重复创建"); } System.out.println("private Singleton1()"); } private static final Singleton1 INSTANCE = new Singleton1(); public static Singleton1 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } public Object readResolve() { return INSTANCE; } }
类一初始化,实例就会提前创建出来。
破坏单例的场景:
1.反射破坏单例
通过调用无参构造方法,将私有的构造方法设为true,使它也可以被使用,然后再创建它的实例。导致它不再是单例。
预防:
在构造方法里加一个判断:如果instance不等于null,则抛出一个异常:单例对象不能重复创建。
2.反序列化破坏单例
把单例对象传给序列化方法,把对象变成字节流,再把字节流还原成一个对象,在反序列化时会构造一个新的对象。
预防:
写一个特殊的方法:readResolve()。里边返回instance。如果发现重写了这个方法,则将这个方法的返回值作为结果返回。
3.Unsafe破坏单例
调用unsafe方法破坏单例,目前没有找到预防方法。
2.枚举类(饿汉式)
public enum Singleton2 { INSTANCE; private Singleton2() { System.out.println("private Singleton2()"); } @Override public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } public static Singleton2 getInstance() { return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
可以很方便地控制对象的个数。
好处:
1.不怕反射(没有无参构造,不能反射创建枚举构造)、反序列化破坏单例。
2.Unsafe可以破坏枚举单例。
3.懒汉式
public class Singleton3 implements Serializable { private Singleton3() { System.out.println("private Singleton3()"); } private static Singleton3 INSTANCE = null; // Singleton3.class public static synchronized Singleton3 getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton3(); } return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
第一次调用getInstance时才会生成实例。
一开始的Instance变量不赋值,在调用时判断Instance是否为null,如果是则创建。不是则直接返回Instance。
线程安全问题:
线程不安全,线程1创建对象后还没来得及赋值,线程2就判断Instance为null,再次创建对象。
解决方法:
在方法上加一个synchronized,相当于在方法上加了一把锁,线程1进入方法后得到一把锁,线程2进入方法时发现锁对象被别人占用,则不执行,挂起。当线程1退出这个方法时会把锁解开,线程2继续执行时线程1的赋值已经完成,所以线程2可以直接使用线程1创建好的instance。(对性能有一定影响。只有首次创建对象时才需要锁,但是创建好对象后只能进一个线程,影响性能。)
首次创建对象时加入线程安全保护:
4.DCL懒汉式(双检锁)
public class Singleton4 implements Serializable { private Singleton4() { System.out.println("private Singleton4()"); } private static volatile Singleton4 INSTANCE = null; // 可见性,有序性 public static Singleton4 getInstance() { if (INSTANCE == null) { synchronized (Singleton4.class) { if (INSTANCE == null) { INSTANCE = new Singleton4(); } } } return INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
在加锁之前先判断instance是否已创建好,如果已创建则直接返回instance,不会进入锁块。如果没有创建才进入锁,解决instance的竞争问题。当线程2拿到锁时,线程1挂起,线程2创建好对象并解锁,线程1经过第二个if判断发现已创建好对象,拿到直接返回的instance。
双检索的方法外面必须加volatile修饰。
解决共享变量的可见性和有序性问题。双检索变量是为了解决有序性问题。
线程1调用对象和给对象赋值的顺序有可能颠倒,可能先给对象赋值再调用对象(里边可能还有很多成员变量没有初始化);此时线程2在线程1给对象赋值后,就判断instance不等于null,然后返回了instance,此时线程1才调用对象。此时线程2拿到的就是不完整的对象。
加上volatile修饰后,volatile会在赋值语句之后加上一个内存屏障,阻止之前的赋值操作越过屏障跑到屏障下边,阻止指令的重排序。构造方法不可能越过屏障跑到赋值下边。
这样的话,即使线程2在线程1完成前进入方法,也无法在赋值完成之前拿到instance,会在锁中等待解锁后才拿到返回值。
5.内部类懒汉式
public class Singleton5 implements Serializable { private Singleton5() { System.out.println("private Singleton5()"); } private static class Holder { static Singleton5 INSTANCE = new Singleton5(); } public static Singleton5 getInstance() { return Holder.INSTANCE; } public static void otherMethod() { System.out.println("otherMethod()"); } }
给静态变量赋值,代码一定会放入静态代码块执行。由静态代码块执行的代码,线程安全问题由jvm保证。
在类的内部创建一个内部类,内部类可以访问外部的私有变量和私有构造。在内部类里创建单例对象,赋值给内部静态变量。创建过程是线程安全的,而且没有用到这个内部类的时候不会初始化instance,所以是懒汉式。
jdk中有哪些地方体现了单例模式
-
Runtime() 体现了饿汉式单例
-
System中的Console方法体现了双检锁懒汉式单例
-
Collections 中的 EmptyNavigableSet 内部类懒汉式单例
-
ReverseComparator.REVERSE_ORDER 内部类懒汉式单例
-