单例模式的奇幻漂流
单例模式这个题目可以关系到很多知识点。比如线程安全、类加载机制、synchronized的原理、volatile的原理、指令重排与内存屏障、枚举的实现、反射与单例模式、序列化如何破坏单例、CAS、CAS的ABA问题、Threadlocal等。
public class Singleton { private static Singleton instance; private Singleton (){} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
线程安全版本的懒汉模式:
public class LazySingleton { private static volatile LazySingleton instance=null; //保证 instance 在所有线程中同步 private LazySingleton(){} //private 避免类在外部被实例化 public static synchronized LazySingleton getInstance() { //getInstance 方法前加同步 if(instance==null) { instance=new LazySingleton(); } return instance; } }
注意:如果编写的是多线程程序,则不能删除代码中的关键字 volatile 和 synchronized,否则将存在线程非安全的问题。如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
饿汉模式单例的变种:
public class Singleton { private Singleton instance = null; static { instance = new Singleton(); } private Singleton (){} public static Singleton getInstance() { return this.instance; } }
饿汉式的创建方式在一些场景中将无法使用:比如 Singleton 实例的创建是依赖参数或者配置文件的,在getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。
public class DoubleCheckSingleton{ private static volatile DoubleCheckSingleton instance;//静止指令重排,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作 private DoubleCheckSingleton(){} public static DoubleCheckSingleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new DoubleCheckSingleton(); } } } return instance; } }
这样写有好处: 检查变量是否被初始化(不去获得锁),如果已被初始化则立即返回, 如果没有获取锁,再次检查变量是否已经被初始化,如果还没被初始化就初始化一个对象。如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。 除了初始化的时候会出现加锁的情况,后续的所有调用都会避免加锁而直接返回,解决了性能消耗的问题,解决了上述的懒汉单例的缺点。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
-
遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:new一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。
-
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
-
当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
-
当使用JDK 1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
public enum Singleton { INSTANCE; public void whateverMethod() { } }
枚举其实底层是依赖Enum类实现的,这个类的成员变量都是static类型的,并且在静态代码块中实例化的,和饿汉有点像, 所以天然是线程安全的。
枚举在java中与普通类一样,都能拥有字段与方法,而且枚举实例创建是线程安全的,在任何情况下,它都是一个单例。可直接以 SingleTon.INSTANCE的方式调用。
public class Singleton { private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); private Singleton() {} public static Singleton getInstance() { for (;;) { Singleton singleton = INSTANCE.get(); if (null != singleton) { return singleton; } singleton = new Singleton();//大量的对象被创建,很容易造成OOM if (INSTANCE.compareAndSet(null, singleton)) { return singleton; } } } }
ThreadLocal的理解:ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制(synchronized)采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。同步机制仅提供一份变量,让不同的线程排队访问,而ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
public class Singleton { private static final ThreadLocal<Singleton> singleton = new ThreadLocal<Singleton>() { @Override protected Singleton initialValue() { return new Singleton(); } }; public static Singleton getInstance() { return singleton.get(); } private Singleton() {} }
public class IdGenerator { private AtomicLong id = new AtomicLong(0); private static IdGenerator instance; private static SharedObjectStorage storage = FileSharedObjectStorage(); private static DistributedLock lock = new DistributedLock(); private IdGenerator() {} public synchronized static IdGenerator getInstance() if (instance == null) { lock.lock(); instance = storage.load(IdGenerator.class); } return instance; } public synchroinzed void freeInstance() { storage.save(this, IdGeneator.class); instance = null; //释放对象 lock.unlock(); } public long getId() { return id.incrementAndGet(); } } // IdGenerator使用举例 IdGenerator idGeneator = IdGenerator.getInstance(); long id = idGenerator.getId(); IdGenerator.freeInstance();
上面是一段伪代码,我们把单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。多个不同进程之间的访问通过分布式锁来控制。
/** * 使用双重校验锁方式实现单例 */ public class Singleton implements Serializable{ private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
public class SingletonTest { public static void main(String[] args) { Singleton singleton = Singleton.getSingleton(); try { Class<Singleton> singleClass = (Class<Singleton>)Class.forName("com.dev.interview.Singleton"); Constructor<Singleton> constructor = singleClass.getDeclaredConstructor(null); constructor.setAccessible(true); Singleton singletonByReflect = constructor.newInstance(); System.out.println("singleton : " + singleton); System.out.println("singletonByReflect : " + singletonByReflect); System.out.println("singleton == singletonByReflect : " + (singleton == singletonByReflect)); } catch (Exception e) { e.printStackTrace(); } } } 输出为: singleton : com.dev.interview.Singleton@55d56113 singletonByReflect : com.dev.interview.Singleton@148080bb singleton == singletonByReflect : false
如上,通过发射的方式即可获取到一个新的单例对象,这就破坏了单例。
private Singleton() { if (singleton != null) { throw new RuntimeException("Singleton constructor is called... "); } }
这样,在通过反射调用构造方法的时候,就会抛出异常:
Caused by: java.lang.RuntimeException: Singleton constructor is called...
public class SingletonTest { public static void main(String[] args) { Singleton singleton = Singleton.getSingleton(); //Write Obj to file ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream("tempFile")); oos.writeObject(singleton); //Read Obj from file File file = new File("tempFile"); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); Singleton singletonBySerialize = (Singleton)ois.readObject(); //判断是否是同一个对象 System.out.println("singleton : " + singleton); System.out.println("singletonBySerialize : " + singletonBySerialize); System.out.println("singleton == singletonBySerialize : " + (singleton == singletonBySerialize)); } catch (Exception e) { e.printStackTrace(); } } } 输出结果如下: singleton : com.dev.interview.Singleton@617faa95 singletonBySerialize : com.dev.interview.Singleton@5d76b067 singleton == singletonBySerialize : false
如上,通过先序列化再反序列化的方式,可获取到一个新的单例对象,这就破坏了单例。
private Object readResolve() { return getSingleton(); }
为什么增加readResolve就可以解决序列化破坏单例的问题了呢?