单例是最多见的设计模式之一,实现的方式很是多,同时须要注意的问题也很是多。html
本文主要内容:java
单例模式(Singleton Pattern):确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象建立型模式。算法
单例模式有三个要点:spring
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance()
工厂方法,让客户能够访问它的惟一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton
类型的静态对象,做为外部共享的惟一实例。设计模式
// 线程安全 public class Singleton { private final static Singleton INSTANCE = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return INSTANCE; } }
优势:简单,使用时没有延迟;在类装载时就完成实例化,天生的线程安全浏览器
缺点:没有懒加载,启动较慢;若是从始至终都没使用过这个实例,则会形成内存的浪费。安全
// 线程安全 public class Singleton { private static Singleton instance; static { instance = new Singleton(); } private Singleton() {} public static Singleton getInstance() { return instance; } }
将类实例化的过程放在了静态代码块中,在类装载的时执行静态代码块中的代码,初始化类的实例。优缺点同上。微信
// 线程不安全 public class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
优势:懒加载,启动速度快、若是从始至终都没使用过这个实例,则不会初始化该实力,可节约资源mybatis
缺点:多线程环境下线程不安全。if (singleton == null)
存在竞态条件,可能会有多个线程同时进入 if 语句
,致使产生多个实例多线程
// 线程安全,效率低 public class Singleton { private static Singleton singleton; private Singleton() {} public static synchronized Singleton getInstance() { if (singleton == null) { singleton = new Singleton(); } return singleton; } }
优势:解决了上一种实现方式的线程不安全问题
缺点:synchronized 对整个 getInstance()
方法都进行了同步,每次只有一个线程可以进入该方法,并发性能极差
// 线程安全 public class Singleton { // 注意:这里有 volatile 关键字修饰 private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
优势:线程安全;延迟加载;效率较高。
因为 JVM 具备指令重排的特性,在多线程环境下可能出现 singleton 已经赋值但还没初始化的状况,致使一个线程得到尚未初始化的实例。volatile 关键字的做用:
// 线程安全 public class Singleton { private Singleton() {} private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } }
优势:避免了线程不安全,延迟加载,效率高。
静态内部类的方式利用了类装载机制来保证线程安全,只有在第一次调用getInstance方法时,才会装载SingletonInstance内部类,完成Singleton的实例化,因此也有懒加载的效果。
加入参数 -verbose:class
能够查看类加载顺序
$ javac Singleton.java $ java -verbose:class Singleton
// 线程安全 public enum Singleton { INSTANCE; public void whateverMethod() { } }
优势:经过JDK1.5中添加的枚举来实现单例模式,写法简单,且不只能避免多线程同步问题,并且还能防止反序列化从新建立新的对象。
单例模式的目标是,任什么时候候该类都只有惟一的一个对象。可是上面咱们写的大部分单例模式都存在漏洞,被攻击时会产生多个对象,破坏了单例模式。
经过Java的序列化机制来攻击单例模式
public class HungrySingleton { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; } public static void main(String[] args) throws IOException, ClassNotFoundException { HungrySingleton singleton = HungrySingleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file")); oos.writeObject(singleton); // 序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file")); HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); // 反序列化 System.out.println(singleton); System.out.println(newSingleton); System.out.println(singleton == newSingleton); } }
结果
com.singleton.HungrySingleton@ed17bee com.singleton.HungrySingleton@46f5f779 false
Java 序列化是如何攻击单例模式的呢?咱们须要先复习一下Java的序列化机制
java.io.ObjectOutputStream
是Java实现序列化的关键类,它能够将一个对象转换成二进制流,而后能够经过 ObjectInputStream
将二进制流还原成对象。具体的序列化过程不是本文的重点,在此仅列出几个要点。
Java 序列化机制的要点:
java.io.Serializable
接口,不然会抛出NotSerializableException
异常serialVersionUID
变量,Java序列化机制会根据编译时的class自动生成一个serialVersionUID
做为序列化版本比较(验证一致性),若是检测到反序列化后的类的serialVersionUID
和对象二进制流的serialVersionUID
不一样,则会抛出异常java.io.Serializable
接口transient
后,默认序列化机制就会忽略该字段,反序列化后自动得到0或者null值readObject
、writeObject
方法实现本身的序列化策略,即便是transient
修饰的成员变量也能够手动调用ObjectOutputStream
的writeInt
等方法将这个成员变量序列化。private Object readResolve()
方法,在调用readObject
方法以后,若是存在readResolve
方法则自动调用该方法,readResolve
将对readObject
的结果进行处理,而最终readResolve
的处理结果将做为readObject
的结果返回。readResolve
的目的是保护性恢复对象,其最重要的应用就是保护性恢复单例、枚举类型的对象Serializable
接口是一个标记接口,可自动实现序列化,而Externalizable
继承自Serializable
,它强制必须手动实现序列化和反序列化算法,相对来讲更加高效根据上面对Java序列化机制的复习,咱们能够自定义一个 readResolve
,在其中返回类的单例对象,替换掉 readObject
方法反序列化生成的对象,让咱们本身写的单例模式实现保护性恢复对象
public class HungrySingleton implements Serializable { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; } private Object readResolve() { return instance; } public static void main(String[] args) throws IOException, ClassNotFoundException { HungrySingleton singleton = HungrySingleton.getInstance(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file")); HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); System.out.println(singleton); System.out.println(newSingleton); System.out.println(singleton == newSingleton); } }
再次运行
com.singleton.HungrySingleton@24273305 com.singleton.HungrySingleton@24273305 true
注意:本身实现的单例模式都须要避免被序列化破坏
在单例模式中,构造器都是私有的,而反射能够经过构造器对象调用 setAccessible(true)
来得到权限,这样就能够建立多个对象,来破坏单例模式了
public class HungrySingleton { private static final HungrySingleton instance = new HungrySingleton(); private HungrySingleton() { } public static HungrySingleton getInstance() { return instance; } public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { HungrySingleton instance = HungrySingleton.getInstance(); Constructor constructor = HungrySingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); // 得到权限 HungrySingleton newInstance = (HungrySingleton) constructor.newInstance(); System.out.println(instance); System.out.println(newInstance); System.out.println(instance == newInstance); } }
输出结果
com.singleton.HungrySingleton@3b192d32 com.singleton.HungrySingleton@16f65612 false
反射是经过它的Class对象来调用构造器建立新的对象,咱们只须要在构造器中检测并抛出异常就能够达到目的了
private HungrySingleton() { // instance 不为空,说明单例对象已经存在 if (instance != null) { throw new RuntimeException("单例模式禁止反射调用!"); } }
运行结果
Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.singleton.HungrySingleton.main(HungrySingleton.java:32) Caused by: java.lang.RuntimeException: 单例模式禁止反射调用! at com.singleton.HungrySingleton.<init>(HungrySingleton.java:20) ... 5 more
注意,上述方法针对饿汉式单例模式是有效的,但对懒汉式的单例模式是无效的,懒汉式的单例模式是没法避免反射攻击的!
为何对饿汉有效,对懒汉无效?由于饿汉的初始化是在类加载的时候,反射必定是在饿汉初始化以后才能使用;而懒汉是在第一次调用 getInstance()
方法的时候才初始化,咱们没法控制反射和懒汉初始化的前后顺序,若是反射在前,无论反射建立了多少对象,instance都将一直为null,直到调用 getInstance()
。
事实上,实现单例模式的惟一推荐方法,是使用枚举类来实现。
写下咱们的枚举单例模式
package com.singleton; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public enum SerEnumSingleton implements Serializable { INSTANCE; // 单例对象 private String content; public String getContent() { return content; } public void setContent(String content) { this.content = content; } private SerEnumSingleton() { } public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { SerEnumSingleton singleton1 = SerEnumSingleton.INSTANCE; singleton1.setContent("枚举单例序列化"); System.out.println("枚举序列化前读取其中的内容:" + singleton1.getContent()); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj")); oos.writeObject(singleton1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerEnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); SerEnumSingleton singleton2 = (SerEnumSingleton) ois.readObject(); ois.close(); System.out.println(singleton1 + "\n" + singleton2); System.out.println("枚举序列化后读取其中的内容:" + singleton2.getContent()); System.out.println("枚举序列化先后两个是否同一个:" + (singleton1 == singleton2)); Constructor<SerEnumSingleton> constructor = SerEnumSingleton.class.getDeclaredConstructor(); constructor.setAccessible(true); SerEnumSingleton singleton3 = constructor.newInstance(); // 经过反射建立对象 System.out.println("反射后读取其中的内容:" + singleton3.getContent()); System.out.println("反射先后两个是否同一个:" + (singleton1 == singleton3)); } }
运行结果,序列化先后的对象是同一个对象,而反射的时候抛出了异常
枚举序列化前读取其中的内容:枚举单例序列化 INSTANCE INSTANCE 枚举序列化后读取其中的内容:枚举单例序列化 枚举序列化先后两个是否同一个:true Exception in thread "main" java.lang.NoSuchMethodException: com.singleton.SerEnumSingleton.<init>() at java.lang.Class.getConstructor0(Class.java:3082) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at com.singleton.SerEnumSingleton.main(SerEnumSingleton.java:39)
编译后,再经过 JAD 进行反编译获得下面的代码
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) // Source File Name: SerEnumSingleton.java package com.singleton; import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; public final class SerEnumSingleton extends Enum implements Serializable { public static SerEnumSingleton[] values() { return (SerEnumSingleton[])$VALUES.clone(); } public static SerEnumSingleton valueOf(String name) { return (SerEnumSingleton)Enum.valueOf(com/singleton/SerEnumSingleton, name); } public String getContent() { return content; } public void setContent(String content) { this.content = content; } private SerEnumSingleton(String s, int i) { super(s, i); } public static void main(String args[]) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { SerEnumSingleton singleton1 = INSTANCE; singleton1.setContent("\u679A\u4E3E\u5355\u4F8B\u5E8F\u5217\u5316"); System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton1.getContent()).toString()); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj")); oos.writeObject(singleton1); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SerEnumSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); SerEnumSingleton singleton2 = (SerEnumSingleton)ois.readObject(); ois.close(); System.out.println((new StringBuilder()).append(singleton1).append("\n").append(singleton2).toString()); System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton2.getContent()).toString()); System.out.println((new StringBuilder()).append("\u679A\u4E3E\u5E8F\u5217\u5316\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton2).toString()); Constructor constructor = com/singleton/SerEnumSingleton.getDeclaredConstructor(new Class[0]); constructor.setAccessible(true); SerEnumSingleton singleton3 = (SerEnumSingleton)constructor.newInstance(new Object[0]); System.out.println((new StringBuilder()).append("\u53CD\u5C04\u540E\u8BFB\u53D6\u5176\u4E2D\u7684\u5185\u5BB9\uFF1A").append(singleton3.getContent()).toString()); System.out.println((new StringBuilder()).append("\u53CD\u5C04\u524D\u540E\u4E24\u4E2A\u662F\u5426\u540C\u4E00\u4E2A\uFF1A").append(singleton1 == singleton3).toString()); } public static final SerEnumSingleton INSTANCE; private String content; private static final SerEnumSingleton $VALUES[]; static { INSTANCE = new SerEnumSingleton("INSTANCE", 0); $VALUES = (new SerEnumSingleton[] { INSTANCE }); } }
经过反编译后代码咱们能够看到,ublic final class T extends Enum
,说明,当咱们使用enmu来定义一个枚举类型的时候,编译器会自动帮咱们建立一个final类型的类继承Enum类,因此枚举类型不能被继承。
1. 枚举单例写法简单
2. 线程安全&懒加载
代码中 INSTANCE 变量被 public static final
修饰,由于static类型的属性是在类加载以后初始化的,JVM能够保证线程安全;且Java类是在引用到的时候才进行类加载,因此枚举单例也有懒加载的效果。
3. 枚举本身能避免序列化攻击
为了保证枚举类型像Java规范中所说的那样,每个枚举类型极其定义的枚举变量在JVM中都是惟一的,在枚举类型的序列化和反序列化上,Java作了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是经过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不容许任何对这种序列化机制的定制,所以禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 咱们看一下Enum类的valueOf方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name); }
从代码中能够看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()
方法返回的map中获取名字为name的枚举对象,若是不存在就会抛出异常。再进一步跟到enumConstantDirectory()
方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面咱们看到的编译器为咱们建立的那个方法,而后用返回结果填充enumType这个Class对象中的enumConstantDirectory
属性。因此,JVM对序列化有保证。
4. 枚举可以避免反射攻击,由于反射不支持建立枚举对象
Constructor
类的 newInstance
方法中会判断是否为 enum,如果会抛出异常
@CallerSensitive public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } // 不能为 ENUM,不然抛出异常:不能经过反射建立 enum 对象 if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
单例模式做为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率至关高,在不少应用软件和框架中都得以普遍应用。
JDK Runtime类表明着Java程序的运行时环境,每一个Java程序都有一个Runtime实例,该类会被自动建立,咱们能够经过 Runtime.getRuntime() 方法来获取当前程序的Runtime实例。一旦获得了一个当前的Runtime对象的引用,就能够调用Runtime对象的方法去控制Java虚拟机的状态和行为。
Runtime 应用了饿汉式单例模式
public class Runtime { private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } private Runtime() { } //.... }
API 介绍
addShutdownHook(Thread hook) 注册新的虚拟机来关闭挂钩。 availableProcessors() 向 Java 虚拟机返回可用处理器的数目。 exec(String command) 在单独的进程中执行指定的字符串命令。 exec(String[] cmdarray) 在单独的进程中执行指定命令和变量。 exec(String[] cmdarray, String[] envp) 在指定环境的独立进程中执行指定命令和变量。 exec(String[] cmdarray, String[] envp, File dir) 在指定环境和工做目录的独立进程中执行指定的命令和变量。 exec(String command, String[] envp) 在指定环境的单独进程中执行指定的字符串命令。 exec(String command, String[] envp, File dir) 在有指定环境和工做目录的独立进程中执行指定的字符串命令。 exit(int status) 经过启动虚拟机的关闭序列,终止当前正在运行的 Java 虚拟机。 freeMemory() 返回 Java 虚拟机中的空闲内存量。 gc() 运行垃圾回收器。 getRuntime() 返回与当前 Java 应用程序相关的运行时对象。 halt(int status) 强行终止目前正在运行的 Java 虚拟机。 load(String filename) 加载做为动态库的指定文件名。 loadLibrary(String libname) 加载具备指定库名的动态库。 maxMemory() 返回 Java 虚拟机试图使用的最大内存量。 removeShutdownHook(Thread hook) 取消注册某个先前已注册的虚拟机关闭挂钩。 runFinalization() 运行挂起 finalization 的全部对象的终止方法。 totalMemory() 返回 Java 虚拟机中的内存总量。 traceInstructions(on) 启用/禁用指令跟踪。 traceMethodCalls(on) 启用/禁用方法调用跟踪。
Desktop 类容许 Java 应用程序启动已在本机桌面上注册的关联应用程序,以处理 URI 或文件。支持的操做包括:
Desktop 经过一个容器来管理单例对象
public class Desktop { // synchronized 同步方法 public static synchronized Desktop getDesktop(){ if (GraphicsEnvironment.isHeadless()) throw new HeadlessException(); if (!Desktop.isDesktopSupported()) { throw new UnsupportedOperationException("Desktop API is not " + "supported on the current platform"); } sun.awt.AppContext context = sun.awt.AppContext.getAppContext(); Desktop desktop = (Desktop)context.get(Desktop.class); // 获取单例对象 // 存在则返回,不存在则建立,建立后put进容器 if (desktop == null) { desktop = new Desktop(); context.put(Desktop.class, desktop); } return desktop; }
AppContext 中有一个 HashMap 对象table,是实际的容器对象
private final Map<Object, Object> table = new HashMap();
AbstractFactoryBean 类
public final T getObject() throws Exception { if (this.isSingleton()) { return this.initialized ? this.singletonInstance : this.getEarlySingletonInstance(); } else { return this.createInstance(); } } private T getEarlySingletonInstance() throws Exception { Class<?>[] ifcs = this.getEarlySingletonInterfaces(); if (ifcs == null) { throw new FactoryBeanNotInitializedException(this.getClass().getName() + " does not support circular references"); } else { if (this.earlySingletonInstance == null) { // 经过代理建立对象 this.earlySingletonInstance = Proxy.newProxyInstance(this.beanClassLoader, ifcs, new AbstractFactoryBean.EarlySingletonInvocationHandler()); } return this.earlySingletonInstance; } }
ErrorContext 类,经过 ThreadLocal 管理单例对象,一个线程一个ErrorContext对象,ThreadLocal能够保证线程安全
public class ErrorContext { private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>(); private ErrorContext() { } public static ErrorContext instance() { ErrorContext context = LOCAL.get(); if (context == null) { context = new ErrorContext(); LOCAL.set(context); } return context; } //... }
参考:
http://www.hollischuang.com/archives/197
http://www.javashuo.com/article/p-ozksreer-gs.html
https://blog.csdn.net/abc123lzf/article/details/82318148
欢迎评论、转发、分享,您的支持是我最大的动力
更多内容可访问个人我的博客:http://laijianfeng.org
关注【小旋锋】微信公众号,及时接收博文推送