单例模式
日头没有辜负咱们,咱们也切莫辜负日头。——沈从文前端
代码世界中也存在如下顺口溜:java
我单身,我骄傲,我为国家省套套。 我单身,我自豪,我为祖国省橡胶。
单例模式虽然简单,但真正懂的内行的人并很少,今天挑战全网最全的经典设计模式之单例模式。后端
1. 单例模式
定义设计模式
确保一个类在任何状况下都绝对只有一个实例,并提供一个全局访问点。
隐藏其构造方法
属于建立型设计模式缓存
适用场景安全
确保任何状况下都绝对只有一个实例
ServletContext、ServletConfig、ApplicationContext、DBPoolide
2. 饿汉式单例
定义性能
系统初始化的时候就加载,无论有没有用到这个单例。优化
优势ui
执行效率高,性能高,没有任何的锁
缺点
某些状况下,可能会形成内存浪费
可以被反射破坏
代码
public class HungrySingleton { private static final HungrySingleton singleton = new HungrySingleton(); private HungrySingleton(){} public static HungrySingleton getInstance() { return singleton; } }
3. 懒汉式单例
定义
系统初始化的时候不建立实例,只有用到的时候才建立实例。
优势
节省了内存
缺点
synchronized形成性能低下
可以被反射破坏
3.1 方法加锁写法
代码
public class LazySingleton { private static LazySingleton singleton = null; private LazySingleton(){} /** * 版本1 * @return */ private synchronized LazySingleton getInstance() { if (null == singleton) { singleton = new LazySingleton(); } return singleton; } }
3.2 代码块加锁写法
代码
public class LazySingleton { private static LazySingleton singleton = null; private LazySingleton(){} /** * 版本2 相比版本1优化一点点 * @return */ private LazySingleton getInstance() { synchronized (LazySingleton.class) { if (null == singleton) { singleton = new LazySingleton(); } } return singleton; } }
3.3 双重判断加锁写法
陷阱案例
public class LazySingleton { private static LazySingleton singleton = null; private LazySingleton(){} /** * 版本3 双重判断 * @return */ private LazySingleton getInstance() { if (null == singleton) { synchronized (LazySingleton.class) { if (null == singleton) { singleton = new LazySingleton(); } } } return singleton; } }
版本3看起来相比版本2优化了很多,但其实这种双重判断在生产环境有一个极大的漏洞陷阱,就是指令重排序,有须要了解的能够在评论区留言。解决方案也很简单,就是 volatile 关键字。它能够限制指令重排序。
正确写法
public class LazySingleton { private volatile static LazySingleton singleton = null; private LazySingleton(){} /** * 版本3 双重判断 * @return */ private LazySingleton getInstance() { if (null == singleton) { synchronized (LazySingleton.class) { if (null == singleton) { singleton = new LazySingleton(); } } } return singleton; } }
双重判断的优势:性能高了,线程安全了。
缺点:代码可读性极差,不够优雅。
3.4 静态内部类写法
利用JVM加载类的顺序,静态内部类,只有用到的时候外部类用到静态内部类的时候才会加载。
优势
写法优雅,利用了Java的语法特色,性能高,避免了内存浪费
缺点
可以被反射破坏
public class LazyStaticInnerSingleton { private LazyStaticInnerSingleton(){} public static LazyStaticInnerSingleton getInstance() { return LazyHolder.INSTANCE; } private static class LazyHolder { private static final LazyStaticInnerSingleton INSTANCE = new LazyStaticInnerSingleton(); } }
这种写法原本应该够优雅,够完美,可是却有一个缺点是能被反射破坏,文章最后我会证实什么是能被反射破坏。那有没有写法能让这个单例不会被反射破坏?答案是有的!
public class LazyStaticInnerSingleton { private LazyStaticInnerSingleton(){ if (null != LazyHolder.INSTANCE) { throw new RuntimeException("不容许非法访问!"); } } public static LazyStaticInnerSingleton getInstance() { return LazyHolder.INSTANCE; } private static class LazyHolder { private static final LazyStaticInnerSingleton INSTANCE = new LazyStaticInnerSingleton(); } }
这种写法就解决了被反射破坏的问题。可是看起来不是那么的优雅。
4. 注册式单例
定义
将每个实例都缓存到统一的容器中,使用惟一标识获取实例。
4.1. 枚举写法注册式单例
优势
写法优雅,线程安全
缺点
和饿汉式相似,大量使用会形成内存浪费,根本缘由在于枚举自己的特色。
public enum EnumSingleton { INSTANCE; private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance() { return INSTANCE; } }
使用方法
public class Test { public static void main(String[] args) { EnumSingleton singleton = EnumSingleton.getInstance(); singleton.setData(new Object()); singleton.getData(); } }
4.2. Spring IOC容器注册式单例
Spring设计者结合枚举式单例的写法和特色,写了一种本身的IOC 容器注册式单例。
public class ContainerSingleton { private ContainerSingleton() {} private static Map<String, Object> ioc = new ConcurrentHashMap<>(); public Object getInstance(String className) { if (!ioc.containsKey(className)) { Object instance = null; try { instance = Class.forName(className).newInstance(); } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) { e.printStackTrace(); } return instance; } else { return ioc.get(className); } } }
5. ThreadLocal单例
ThreadLocal单例确定会用到ThreadLocal,根据ThreadLocal自己的特色,即同一线程内数据可见,那么这种单例就有自己的局限性,使用的不多。我曾经在token登录的时候用到过。即前端会传一个token到后端,token能解析出登录用户的信息。把解析后的信息放在ThreadLocal中,那么本次处理请求就能在任何地方获取登录用户信息。
public class ThreadLocalSingleton { private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){ @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; private ThreadLocalSingleton() {} public static ThreadLocalSingleton getInstance() { return threadLocalInstance.get(); } }
6. 反射破坏单例证实
public class Test1 { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Class clazz = HungrySingleton.class; Constructor c = clazz.getDeclaredConstructor(null); c.setAccessible(true); Object o1 = c.newInstance(); Object o2 = c.newInstance(); System.out.println(o1 == o2);//会输出false } }
解决方案就是:构造方法抛异常。
if (null != LazyHolder.INSTANCE) { throw new RuntimeException("不容许非法访问!"); }
7. 高高高手须要知道的-序列化破坏单例
首先你必须知道什么是序列化。序列化就是JVM内存中的对象,序列化到磁盘文件,再读取到内存,不一样进程的数据交互须要序列化才能传输。
以上的全部单例模式,解决了各类各样的问题,但都存在同一个问题,就是都会被序列化破坏。意思就是:系统中的单例,被序列化到磁盘,而后再加载到内存,那么这序列化先后两个单例,并非同一个单例。这就是序列化破坏单例。
解决方案:在单例中加入如下方法:
private Object readResolve() { // instead of the object we're on, // return the class variable INSTANCE return INSTANCE; }
最后
感谢您阅读本文,若是您以为文章写的对您有用的话,请您点击上面的“关注”,点个赞,亦可关注公众号《AIO生活》,这样您就能够第一时间收到个人最新文章。
文章内容属于本身的一点点心得,不免有不对的地方,欢迎在下方评论区探讨,您的关注是我创做优质文章的动力。