-
单例模式应用场景
2016-05-06 14:38:16单例模式应用场景 -
单例模式应用场景_php单例模式的常见应用场景
2020-12-14 05:24:26单例模式(Singleton)也叫单态模式,是...这里又不具体讲如何实现单例模式和介绍其原理(因为这方便的已经有太多的好文章介绍了)好多没怎么使用过的人可能会想,单例模式感觉不怎么用到,实际的应用场景有哪些呢?以...单例模式(Singleton)也叫单态模式,是设计模式中最为简单的一种模式,甚至有些模式大师都不称其为模式,称其为一种实现技巧,因为设计模式讲究对象之间的关系的抽象,而单例模式只有自己一个对象,也因此有些设计大师并把把其称为设计模式之一。
这里又不具体讲如何实现单例模式和介绍其原理(因为这方便的已经有太多的好文章介绍了)
好多没怎么使用过的人可能会想,单例模式感觉不怎么用到,实际的应用场景有哪些呢?以下,我将列出一些就在咱们周边和很有意义的单例应用场景。
1. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
3. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
4. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
5. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
7. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
8. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
9. HttpApplication 也是单位例的典型应用。熟悉http://ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.
总结以上,不难看出:
单例模式应用的场景一般发现在以下条件下:
(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
(2)控制资源的情况下,方便资源之间的互相通信。如线程池等。
以上内容希望帮助到大家,很多PHPer在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql优化、shell脚本、Docker、微服务、Nginx等多个知识点高级进阶干货需要的可以免费分享给大家,需要戳这里PHP进阶架构师>>>视频、面试文档免费获取
或 者关注我每天分享技术文章
进阶PHP架构师www.zhihu.com作者:lansedongqing 来源:https://www.cnblogs.com/lansetianko
-
单例模式应用场景_三、单例模式详解
2020-11-28 01:02:03课程目标1、掌握单例模式的应用场景。2、掌握IDEA环境下的多线程调试方式。3、掌握保证线程安全的单例模式策略。4、掌握反射暴力攻击单例解决方案及原理分析。5、序列化破坏单例的原理及解决方案。6、掌握常见的单例...4.单例模式详解
4.1.课程目标
1、掌握单例模式的应用场景。
2、掌握IDEA环境下的多线程调试方式。
3、掌握保证线程安全的单例模式策略。
4、掌握反射暴力攻击单例解决方案及原理分析。
5、序列化破坏单例的原理及解决方案。
6、掌握常见的单例模式写法。
4.2.内容定位
1、听说过单例模式,但不知道如何应用的人群。
2、单例模式是非常经典的高频面试题,希望通过面试单例彰显技术深度,顺利拿到Offer的人群。
4.3.单例模式的应用场景
单例模式(SingletonPattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,公司CEO、部门经 理 等 。 J2EE 标 准 中 的 ServletContext 、 ServletContextConfig 等 、 Spring 框 架 应 用 中 的 ApplicationContext、数据库的连接池BDPool等也都是单例形式。
4.4.饿汉式单例模式
方法1.静态方法获得私有成员对象
/** * 优点:执行效率高,性能高,没有任何的锁 * 缺点:某些情况下,可能会造成内存浪费 */ public class HungrySingleton { //先静态、后动态 //先属性、后方法 //先上后下 private static final HungrySingleton hungrySingleton = new HungrySingleton(); private HungrySingleton(){} public static HungrySingleton getInstance(){ return hungrySingleton; } }
方法2.利用静态代码块与类同时加载的特性生成单例对象
//饿汉式静态块单例模式 public class HungryStaticSingleton { //先静态后动态 //先上,后下 //先属性后方法 private static final HungryStaticSingleton hungrySingleton; //装个B static { hungrySingleton = new HungryStaticSingleton(); } private HungryStaticSingleton(){} public static HungryStaticSingleton getInstance(){ return hungrySingleton; } }
类结构图
优缺点
优点:没有加任何锁、执行效率比较高,用户体验比懒汉式单例模式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能“占着茅坑不拉屎”。
源码
Spring中IoC容器ApplicationContext本身就是典型的饿汉式单例模式
4.5.懒汉式单例模式
特点
懒汉式单例模式的特点是:被外部类调用的时候内部类才会加载。
方法1.加大锁
/** * 优点:节省了内存,线程安全 * 缺点:性能低 */ //懒汉式单例模式在外部需要使用的时候才进行实例化 public class LazySimpleSingletion { private static LazySimpleSingletion instance; //静态块,公共内存区域 private LazySimpleSingletion(){} public synchronized static LazySimpleSingletion getInstance(){ if(instance == null){ instance = new LazySimpleSingletion(); } return instance; } } public class ExectorThread implements Runnable { public void run() { LazySimpleSingletion instance = LazySimpleSingletion.getInstance(); System.out.println(Thread.currentThread().getName() + ":" + instance); } } public class LazySimpleSingletonTest { public static void main(String[] args) { Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("End"); } }
给getInstance()加上synchronized关键字,使这个方法变成线程同步方法:
当执行其中一个线程并调用getInstance()方法时,另一个线程在调用getInstance() 方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完,第二个线程 才恢复到RUNNING状态继续调用getInstance()方法
线程切换调试
上图完美地展现了 synchronized 监视锁的运行状态,线程安全的问题解决了。但是,用 synchronized加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,则会导致大批线程阻塞, 从而导致程序性能大幅下降。那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢? 答案是肯定的。我们来看双重检查锁的单例模式:
方法2.双重检查锁
/** * 优点:性能高了,线程安全了 * 缺点:可读性难度加大,不够优雅 */ public class LazyDoubleCheckSingleton { // volatile解决指令重排序 private volatile static LazyDoubleCheckSingleton instance; private LazyDoubleCheckSingleton() { } public static LazyDoubleCheckSingleton getInstance() { //检查是否要阻塞,第一个instance == null是为了创建后不再走synchronized代码,提高效率。可以理解是个开关。创建后这个开关就关上,后面的代码就不用执行了。 if (instance == null) { synchronized (LazyDoubleCheckSingleton.class) { //检查是否要重新创建实例 if (instance == null) { instance = new LazyDoubleCheckSingleton(); //指令重排序的问题 //1.分配内存给这个对象 //2.初始化对象 //3.设置 lazy 指向刚分配的内存地址 } } } return instance; } } public class ExectorThread implements Runnable { public void run() { LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance(); System.out.println(Thread.currentThread().getName() + ":" + instance); } } public class LazySimpleSingletonTest { public static void main(String[] args) { Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("End"); } }
当第一个线程调用 getInstance()方法时,第二个线程也可以调用。当第一个线程执行到 synchronized时会上锁,第二个线程就会变成 MONITOR状态,出现阻塞。此时,阻塞并不是基于整 个LazySimpleSingleton类的阻塞,而是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于 调用者而言感知不到。
但是,用到 synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。难道就真的没有更好的方案吗?当然有。我们可以从类初始化的角度来考虑,看下面的代码,采用静态内部类的方式:
方法3.静态内部类
/* ClassPath : LazyStaticInnerClassSingleton.class LazyStaticInnerClassSingleton$LazyHolder.class 优点:写法优雅,利用了Java本身语法特点,性能高,避免了内存浪费,不能被反射破坏 缺点:不优雅 */ //这种形式兼顾饿汉式单例模式的内存浪费问题和 synchronized 的性能问题 //完美地屏蔽了这两个缺点 //自认为史上最牛的单例模式的实现方式 public class LazyStaticInnerClassSingleton { //使用 LazyInnerClassGeneral 的时候,默认会先初始化内部类 //如果没使用,则内部类是不加载的 private LazyStaticInnerClassSingleton(){ // if(LazyHolder.INSTANCE != null){ // throw new RuntimeException("不允许非法创建多个实例"); // } } //每一个关键字都不是多余的,static 是为了使单例的空间共享,保证这个方法不会被重写、重载 private static LazyStaticInnerClassSingleton getInstance(){ //在返回结果以前,一定会先加载内部类 return LazyHolder.INSTANCE; } //默认不加载 private static class LazyHolder{ private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton(); } }
这种方式兼顾了饿汉式单例模式的内存浪费问题和 synchronized 的性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。由于这种方式比较简单,我们就不带大家一步一步 调试了。
内部类语法特性 : 内部类用时才加载
4.6.反射破坏单例
public class ReflectTest { public static void main(String[] args) { try { //在很无聊的情况下,进行破坏 Class> clazz = LazyStaticInnerClassSingleton.class; //通过反射获取私有的构造方法 Constructor c = clazz.getDeclaredConstructor(null); //强制访问 c.setAccessible(true); //暴力初始化 Object instance1 = c.newInstance(); //调用了两次构造方法,相当于“new”了两次,犯了原则性错误 Object instance2 = c.newInstance(); System.out.println(instance1); System.out.println(instance2); System.out.println(instance1 == instance2); // Enum }catch (Exception e){ e.printStackTrace(); } } } com.gupaoedu.vip.pattern.singleton.lazy.LazyStaticInnerClassSingleton@64cee07 com.gupaoedu.vip.pattern.singleton.lazy.LazyStaticInnerClassSingleton@1761e840 false
大家有没有发现,上面介绍的单例模式的构造方法除了加上 private 关键字,没有做任何处理。如 果我们使用反射来调用其构造方法,再调用 getInstance()方法,应该有两个不同的实例。现在来看一 段测试代码,以LazyInnerClassSingleton为例:
显然,创建了两个不同的实例。现在,我们在其构造方法中做一些限制,一旦出现多次重复创建, 则直接抛出异常。所以需要在私有构造方法添加异常:
private LazyStaticInnerClassSingleton(){ if(LazyHolder.INSTANCE != null){ throw new RuntimeException("不允许非法创建多个实例"); } }
4.7.序列化破坏单例(扩展知识)
一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象 并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化 的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码:
//反序列化导致破坏单例模式 public class SeriableSingleton implements Serializable { //序列化 //把内存中对象的状态转换为字节码的形式 //把字节码通过IO输出流,写到磁盘上 //永久保存下来,持久化 //反序列化 //将持久化的字节码内容,通过IO输入流读到内存中来 //转化成一个Java对象 // 饿汉式 public final static SeriableSingleton INSTANCE = new SeriableSingleton(); private SeriableSingleton(){} public static SeriableSingleton getInstance(){ return INSTANCE; } // private Object readResolve(){ return INSTANCE;} } public class SeriableSingletonTest { public static void main(String[] args) { SeriableSingleton s1 = null; SeriableSingleton s2 = SeriableSingleton.getInstance(); FileOutputStream fos = null; try { fos = new FileOutputStream("SeriableSingleton.obj"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(s2); oos.flush(); oos.close(); FileInputStream fis = new FileInputStream("SeriableSingleton.obj"); ObjectInputStream ois = new ObjectInputStream(fis); s1 = (SeriableSingleton)ois.readObject(); ois.close(); System.out.println(s1); System.out.println(s2); System.out.println(s1 == s2); } catch (Exception e) { e.printStackTrace(); } } } 打印结果: com.gupaoedu.vip.pattern.singleton.seriable.SeriableSingleton@68837a77 com.gupaoedu.vip.pattern.singleton.seriable.SeriableSingleton@4b6995df false
从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单 例模式的设计初衷。那么,我们如何保证在序列化的情况下也能够实现单例模式呢?其实很简单,只需 要增加readResolve()方法即可。
再看运行结果,如下图所示。
com.gupaoedu.vip.pattern.singleton.seriable.SeriableSingleton@4b6995df com.gupaoedu.vip.pattern.singleton.seriable.SeriableSingleton@4b6995df true
大家一定会想:这是什么原因呢?为什么要这样写?看上去很神奇的样子,也让人有些费解。不如 我们一起来看看JDK的源码实现以了解清楚。我们进入ObjectInputStream类的readObject()方法, 代码如下:
public final Object readObject() throws IOException, ClassNotFoundException { if (enableOverride) { return readObjectOverride(); } // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { Object obj = readObject0(false); handles.markDependency(outerHandle, passHandle); ClassNotFoundException ex = handles.lookupException(passHandle); if (ex != null) { throw ex; } if (depth == 0) { vlist.doCallbacks(); } return obj; } finally { passHandle = outerHandle; if (closed && depth == 0) { clear(); } } }
我们发现,在readObject()方法中又调用了重写的readObject0()方法。进入readObject0()方法, 代码如下:
private Object readObject0(boolean unshared) throws IOException { ... case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); ... }
我们看到TC_OBJECT中调用了ObjectInputStream的readOrdinaryObject()方法,看源码:
private Object readOrdinaryObject(boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); desc.checkDeserialize(); Class> cl = desc.forClass(); if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) { throw new InvalidClassException("invalid class descriptor"); } Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); } ... return obj; }
我们发现调用了ObjectStreamClass的isInstantiable()方法,而isInstantiable()方法的代码如下:
boolean isInstantiable() { requireInitialized(); return (cons != null); }
上述代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回true。这意味着只要 有无参构造方法就会实例化。
这时候其实还没有找到加上 readResolve()方法就避免了单例模式被破坏的真正原因。再回到 ObjectInputStream的readOrdinaryObject()方法,继续往下看:
private Object readOrdinaryObject(boolean unshared) throws IOException { if (bin.readByte() != TC_OBJECT) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); desc.checkDeserialize(); Class> cl = desc.forClass(); if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) { throw new InvalidClassException("invalid class descriptor"); } Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); } ... if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { // Filter the replacement object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } handles.setObject(passHandle, obj = rep); } } return obj; }
判断无参构造方法是否存在之后,又调用了hasReadResolveMethod()方法,来看代码:
boolean hasReadResolveMethod() { requireInitialized(); return (readResolveMethod != null); }
上述代码逻辑非常简单,就是判断 readResolveMethod 是否为空,不为空就返回 true。那么 readResolveMethod是在哪里赋值的呢?通过全局查找知道,在私有方法 ObjectStreamClass()中给 readResolveMethod进行了赋值,来看代码:
private final void requireInitialized() { if (!initialized) throw new InternalError("Unexpected call when not initialized"); }
上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在回到 ObjectInputStream 的 readOrdinaryObject()方法继续往下看,如果 readResolve()方法存在则调用 invokeReadResolve()方法,来看代码:
Object invokeReadResolve(Object obj) throws IOException, UnsupportedOperationException { requireInitialized(); if (readResolveMethod != null) { try { return readResolveMethod.invoke(obj, (Object[]) null); } catch (InvocationTargetException ex) { Throwable th = ex.getTargetException(); if (th instanceof ObjectStreamException) { throw (ObjectStreamException) th; } else { throwMiscException(th); throw new InternalError(th); // never reached } } catch (IllegalAccessException ex) { // should not occur, as access checks have been suppressed throw new InternalError(ex); } } else { throw new UnsupportedOperationException(); } }
我们可以看到,在invokeReadResolve()方法中用反射调用了readResolveMethod方法。
通过JDK源码分析我们可以看出,虽然增加 readResolve()方法返回实例解决了单例模式被破坏的 问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率加快,就意味着内存分配开销也会随之增大,难道真的就没办法从根本上解决问题吗?下面讲的注册式单例也许能帮助到你。
为什么添加了readResolve()方法就可以了?
ObjectInputStream源码中,读取文件时写死判断是否有readResolve()方法,有调用这个方法,没有则重新创建对象。
4.8.注册式单例模式
将每一个实例都缓存到统一的容器中,使用唯一表示获取实例。
注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
方法1. 枚举式单例模式
先来看枚举式单例模式的写法,来看代码,创建EnumSingleton类:
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 EnumSingletonTest { public static void main(String[] args) { EnumSingleton instance = EnumSingleton.getInstance(); instance.setData(new Object()); try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(String.class, int.class); c.setAccessible(true); System.out.println(c); Object o = c.newInstance(); System.out.println(o); } catch (Exception e) { e.printStackTrace(); } } }
java.lang.Object@2acf57e3 java.lang.Object@2acf57e3 true
没有做任何处理,我们发现运行结果和预期的一样。那么枚举式单例模式如此神奇,它的神秘之处 在哪里体现呢?下面通过分析源码来揭开它的神秘面纱。
下载一个非常好用的 Java反编译工具 Jad(下载地址:https://varaneckas.com/jad/),解压后 配置好环境变量(这里不做详细介绍),就可以使用命令行调用了。找到工程所在的Class目录,复制 EnumSingleton.class 所在的路径,如下图所示。
然后切换到命令行,切换到工程所在的Class目录,输入命令 jad 并在后面输入复制好的路径,在 Class 目录下会多出一个 EnumSingleton.jad 文件。打开 EnumSingleton.jad 文件我们惊奇地发现有 如下代码:
static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); }
原来,枚举式单例模式在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例模式的实现。至 此,我们还可以试想,序列化能否破坏枚举式单例模式呢?不妨再来看一下 JDK 源码,还是回到 ObjectInputStream的readObject0()方法:
private Object readObject0(boolean unshared) throws IOException { ... case TC_ENUM: return checkResolve(readEnum(unshared)); ... }
我们看到,在readObject0()中调用了readEnum()方法,来看readEnum()方法的代码实现:
private Enum> readEnum(boolean unshared) throws IOException { if (bin.readByte() != TC_ENUM) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); if (!desc.isEnum()) { throw new InvalidClassException("non-enum class: " + desc); } int enumHandle = handles.assign(unshared ? unsharedMarker : null); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(enumHandle, resolveEx); } String name = readString(false); Enum> result = null; Class> cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") Enum> en = Enum.valueOf((Class)cl, name); result = en; } catch (IllegalArgumentException ex) { throw (IOException) new InvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { handles.setObject(enumHandle, result); } } handles.finish(enumHandle); passHandle = enumHandle; return result; }
我们发现,枚举类型其实通过类名和类对象类找到一个唯一的枚举对象。因此,枚举对象不可能被 类加载器加载多次。那么反射是否能破坏枚举式单例模式呢?来看一段测试代码:
public static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(); c.newInstance(); } catch (Exception e) { e.printStackTrace(); } }
运行结果如下图所示。
结果中报的是 java.lang.NoSuchMethodException异常,意思是没找到无参的构造方法。这时候, 我们打开 java.lang.Enum的源码,查看它的构造方法,只有一个protected类型的构造方法,代码如 下:
protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; }
我们再来做一个下面这样的测试:
public static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor c = clazz.getDeclaredConstructor(String.class, int.class); c.setAccessible(true); EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("Tom", 666); } catch (Exception e) { e.printStackTrace(); } }
运行结果如下图所示
这时错误已经非常明显了,“Cannot reflectively create enum objects”,即不能用反射来创建 枚举类型。还是习惯性地想来看看JDK源码,进入Constructor的newInstance()方法:
@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); } } 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; }
从上述代码可以看到,在 newInstance()方法中做了强制性的判断,如果修饰符是Modifier.ENUM 枚举类型,则直接抛出异常。
到此为止,我们是不是已经非常清晰明了呢?枚举式单例模式也是《EffectiveJava》书中推荐的一种单例模式实现写法。JDK枚举的语法特殊性及反射也为枚举保驾护航,让枚举式单例模式成为一种比 较优雅的实现。
枚举源码
java.lang.Enum通过valueOf获得值
public static > T valueOf(Class 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); } Map enumConstantDirectory() { if (enumConstantDirectory == null) { T[] universe = getEnumConstantsShared(); if (universe == null) throw new IllegalArgumentException( getName() + " is not an enum type"); Map m = new HashMap<>(2 * universe.length); for (T constant : universe) m.put(((Enum>)constant).name(), constant); enumConstantDirectory = m; } return enumConstantDirectory; } private volatile transient Map enumConstantDirectory = null;
枚举模式的实例天然具有线程安全性,防止序列化与反射的特性。
有点像饿汉式单例。创建时就将常量存放在map容器中。
优点:写法优雅。加载时就创建对象。线程安全。
缺点:不能大批量创建对象,否则会造成浪费。spring中不能使用它。
结论:如果不是特别重的对象,建议使用枚举单例模式,它是JVM天然的单例。
方法2. 容器式单例
Spring改良枚举写出的改良方法:IOC容器
接下来看注册式单例模式的另一种写法,即容器式单例模式,创建ContainerSingleton类:
public class ContainerSingleton { private ContainerSingleton(){} private static Map ioc = new ConcurrentHashMap(); public static Object getInstance(String className){ Object instance = null; if(!ioc.containsKey(className)){ try { instance = Class.forName(className).newInstance(); ioc.put(className, instance); }catch (Exception e){ e.printStackTrace(); } return instance; }else{ return ioc.get(className); } } }
测试
public class ContainerSingletonTest { public static void main(String[] args) { Object instance1 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo"); Object instance2 = ContainerSingleton.getInstance("com.gupaoedu.vip.pattern.singleton.test.Pojo"); System.out.println(instance1 == instance2); } }
结果
true
容器式单例模式适用于实例非常多的情况,便于管理。但它是非线程安全的。到此,注册式单例模式介绍完毕。我们再来看看Spring中的容器式单例模式的实现代码:
public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory { /** Cache of unfinished FactoryBean instances: FactoryBean name --> BeanWrapper */ private final Map factoryBeanInstanceCache = new ConcurrentHashMap(16); }
容器为啥不能被反射破坏?秩序的维护者,创造了一个生态
4.9.线程单例实现ThreadLocal
最后赠送给大家一个彩蛋,讲讲线程单例实现 ThreadLocal。ThreadLocal 不能保证其创建的对象 是全局唯一的,但是能保证在单个线程中是唯一的,天生是线程安全的。下面来看代码:
public class ThreadLocalSingleton { private static final ThreadLocal threadLocaLInstance = new ThreadLocal(){ @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; private ThreadLocalSingleton(){} public static ThreadLocalSingleton getInstance(){ return threadLocaLInstance.get(); } }
写一下测试代码:
public class ThreadLocalSingletonTest { public static void main(String[] args) { System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); System.out.println(ThreadLocalSingleton.getInstance()); Thread t1 = new Thread(new ExectorThread()); Thread t2 = new Thread(new ExectorThread()); t1.start(); t2.start(); System.out.println("End"); } }
运行结果如下图所示。
com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 com.gupaoedu.vip.pattern.singleton.threadlocal.ThreadLocalSingleton@1761e840 End Thread-0:com.gupaoedu.vip.pattern.singleton.lazy.LazyDoubleCheckSingleton@551f86f1 Thread-1:com.gupaoedu.vip.pattern.singleton.lazy.LazyDoubleCheckSingleton@551f86f1
我们发现,在主线程中无论调用多少次,获取到的实例都是同一个,都在两个子线程中分别获取到 了不同的实例。那么 ThreadLocal是如何实现这样的效果的呢?我们知道,单例模式为了达到线程安全 的目的,会给方法上锁,以时间换空间。ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。
不是线程作为key,而是threadlocal本身。
ThreadLocal源码
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
5.0.源码
AbstractFactoryBean
public final T getObject() throws Exception { if (isSingleton()) { return (this.initialized ? this.singletonInstance : getEarlySingletonInstance()); } else { return createInstance(); } } private T getEarlySingletonInstance() throws Exception { Class[] ifcs = getEarlySingletonInterfaces(); if (ifcs == null) { throw new FactoryBeanNotInitializedException( getClass().getName() + " does not support circular references"); } if (this.earlySingletonInstance == null) { this.earlySingletonInstance = (T) Proxy.newProxyInstance( this.beanClassLoader, ifcs, new EarlySingletonInvocationHandler()); } return this.earlySingletonInstance; }
MyBatis的ErrorContext使用了ThreadLocal
public class ErrorContext { private static final ThreadLocal LOCAL = new ThreadLocal<>(); private ErrorContext() { } public static ErrorContext instance() { ErrorContext context = LOCAL.get(); if (context == null) { context = new ErrorContext(); LOCAL.set(context); } return context; } }
5.0.单例模式小结
单例模式优点:
- 在内存中只有一个实例,减少了内存开销。
- 可以避免资源的多重占用。
- 设置全局访问点,严格控制访问。
单例模式的缺点:
- 没有接口,扩展困难。
- 如果要扩展单例对象,只有修改代码,没有其他途径。
学习单例模式的知识重点总结
- 私有化构造器
- 保证线程安全
单例模式可以保证内存里只有一个实例,减少了内存的开销,还可以避免对资源的多重占用。单例模式看起来非常简单,实现起来其实也非常简单,但是在面试中却是一个高频面试点。希望“小伙伴们” 通过本章的学习,对单例模式有了非常深刻的认识,在面试中彰显技术深度,提升核心竞争力,给面试 加分,顺利拿到录取通知(Offer)。
5.1.作业
1、解决容器式单例的线程安全问题。
两种方法:双重检查锁,利用ConcurrentHashMap#putIfAbsent()方法的原子性。
public class ContainerSingleton { private static Map ioc = new ConcurrentHashMap(); private ContainerSingleton() { throw new RuntimeException("不可被实例化!"); } // 方法一:双重检查锁 public static Object getInstance(String className) { Object instance = null; if (!ioc.containsKey(className)) { synchronized (ContainerSingleton.class) { if (!ioc.containsKey(className)) { try { instance = Class.forName(className).newInstance(); ioc.put(className, instance); } catch (Exception e) { e.printStackTrace(); } return instance; } else { return ioc.get(className); } } } return ioc.get(className); } // 方法二:利用ConcurrentHashMap#putIfAbsent()方法的原子性 public static Object getInstance1(String className){ Object instance = null; try { ioc.putIfAbsent(className, Class.forName(className).newInstance()); }catch (Exception e){ e.printStackTrace(); } return ioc.get(className); } }
-
单例模式应用场景_单例设计模式
2020-12-10 20:18:551.单例模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式2.单例模式有三个特点: 1,单例只有一个实现对象 2,单例对象必须由单例类自行创建 3,单例类对外提供一个访问该单例的全局访问点3.单例...1.单例模式的定义:
指一个类只有一个实例,且该类能自行创建这个实例的一种模式
2.单例模式有三个特点:
1,单例只有一个实现对象
2,单例对象必须由单例类自行创建
3,单例类对外提供一个访问该单例的全局访问点
3.单例模式的结构与实现
单例模式是设计模式中最简单的模式之一,通常普通类的构造函数是公有的,外部类可以通过new构造函数来创建多个实例,但是将类的构造函数设置为私有的,外部类就无法调用该构造函数,也就无法创建实例,这时该类自身必须 定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或该静态私有实例。
4.单例模式的实现
Singleton模式通常有两种实现形式
第一种:懒汉式单你例
该模式的特点是类加载时没有生成单例,只有当第一次调用方法时才去创建这个单例
代码如下:
public class Singleton{ Singleton(){ System.out.println("------new-------"); } } public class ModeTest01{ private static Singleton s2; public static Singleton getS2(){ if(s2 == null){ s2 = new Singleton(); } return s2; } @Test public void test(){ //用多线种来测试一下 for(int i = 0; i<500; i++){ new Thread(){ public void run(){ getS2(); } }.start(); } } } //运行结果
运行结果证明,懒汉模式在多程的情况下,也会多次创建对象,没有做到只有创建一次实例,那要怎么做到线程安全的呢,用同步方法,同步代码块都是可以解决,这代码如下
public static Singleton getS2OK(){ synchronized (Singleton.class) { if(s2 == null){ s2 = new Singleton(); } } return s2; } @Test public void test(){ for (int i = 0; i < 500; i++) { new Thread(){ public void run() { getS2OK(); } }.start(); } } //运行结果
第二种:饿汉式单例
该模式的特点是类一旦加载就创建一个单例,保证在调用方法之前单例已经存在了
饿汉模式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变了,所以是线程安全的,可以直接用于多线程而不会出现问,代码如下
public class ModeTest02 { /** * 饿汉模式:类加载时,直接创建对象,不会有线程安全问题 * */ private static Singleton s1 = new Singleton(); public static Singleton getS1(){ return s1; } @Test public void test(){ for (int i = 0; i < 500; i++) { new Thread(){ public void run() { getS1(); } }.start(); } } } //运行结果
单例模式应该用场景
1.在应用场景中,某类只要求生成一个对象的时候,如一个班中的班长,每个人身份证号等
2.当对象需要被共享的场合,由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如web中配置对象,数据库连接池等
3.当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池。
-
单例模式应用场景_单例模式的7种实现方式及反射,序列化破坏单例模式怎样防止?...
2020-11-29 09:34:40主要包含以下内容单例模式含义单例特点单例缺点应用场景单例实现方式饿汉模式 (静态方法,静态块,枚举)懒汉模式(单线程,线程安全,双重验证,静态内部类)反射破环单例单例序列化单例模式含义单例模式(Singleton ...概述
本文主要记录单例模式各种实现方式。主要包含以下内容
- 单例模式含义
- 单例特点
- 单例缺点
- 应用场景
- 单例实现方式
- 饿汉模式 (静态方法,静态块,枚举)
- 懒汉模式(单线程,线程安全,双重验证,静态内部类)
- 反射破环单例
- 单例序列化
单例模式含义
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象
特点
- 单例模式限制了类的实例化,单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
- 避免对共享资源的多重占用
劣势
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”
应用场景
- 要求生产唯一序列号。
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
单例实现方式为了实现Singleton模式,我们有不同的方法,但是所有方法都具有以下共同概念。
- 私有构造函数,用于限制该类从其他类的实例化。
- 同一类的私有静态变量,是该类的唯一实例。
- 返回类实例的公共静态方法,这是外部世界获取单例类实例的全局访问点。
饿汉模式
静态方法初始化单例类实现
在类加载时,就进行构建,急切初始化。
- 优点
简单。创建单例模式最简单的一种方式。
线程安全。
速度快。由于类加载时已经进行初始化,调用速度快。
- 缺点
浪费资源。没有调用对象,也会创建。
没有异常处理。
public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; }}
静态块实现
实现方式与急切创建类似,在急切创建基础上添加了异常处理。
枚举方式
- 优点
创建枚举默认就是线程安全的。
防止反序列化导致重新创建新的对象。保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)。
effective java 中推荐。
- 不足
灵活性不足
public enum Singleton { INSTANCE;}
懒汉模式(惰性模式)
实现单例模式的惰性初始化方法在全局访问方法中创建实例
- 优点
在单线程中可以实现按需加载。
- 不足
不适用于多线程。 多线程时,可能存在破环单例模式情况。
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}
线程安全单例实现
创建线程安全的单例类的更简单方法是使全局访问方法时加锁,实现同步,以便一次只能有一个线程执行此方法
- 优点
保证多线程下正常运行。
- 不足
性能低下。每次调用时都会进行synchronized ,引起阻塞,导致性能低下。
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}
双重校验
为了在多线程环境下,不影响程序的性能,不让线程每次调用方法时都加锁,而只是在实例未被创建时再加锁,在加锁处理里面还需要判断一次实例是否已存在。
public class Singleton { private static Singleton instance; private Singleton() { } public static ThreadSafeSingleton Singleton() { if (instance == null) { synchronized (ThreadSafeSingleton.class) { if (instance == null) { instance = new ThreadSafeSingleton(); } } } return instance; }}
静态内部类实现 (推荐)
- 优点
资源利用率高,不执行getInstance()不被实例,可以执行该类其他静态方法 。目前使用率最高的方法。
不需要考虑多线程同步问题。
- 不足
第一次加载时反应不够快 。
由于存在静态方法,在类加载时就会创建类。而且会一直存在。
public class Singleton { private Singleton() { } public static Singleton getInstance() { return SingletonHelper.INSTANCE; } private static class SingletonHelper { private static final Singleton INSTANCE = new Singleton(); }}
反射破坏单例
以上的单例模式,除枚举外,在反射时会被破坏。以下代码输出的hashcode值是不一致的。
public class SingletonTest { public static void main(String[] args) { Singleton instanceOne = Singleton.getInstance(); Singleton instanceTwo = null; try { Constructor[] constructors = Singleton.class.getDeclaredConstructors(); for (Constructor constructor : constructors) { constructor.setAccessible(true); instanceTwo = (Singleton) constructor.newInstance(); break; } } catch (Exception e) { } System.out.println(instanceOne.hashCode()); System.out.println(instanceTwo.hashCode()); }}
单例模式序列化
序列化单例类的问题在于,每当我们反序列化它时,它将创建该类的新实例。为防止破环单例模式,加入readResolve()函数。
public class SerializedSingleton implements Serializable { private SerializedSingleton() { } public static SerializedSingleton getInstance() { return SingletonHelper.INSTANCE; } protected Object readResolve() { return getInstance(); } private static class SingletonHelper { private static final SerializedSingleton INSTANCE = new SerializedSingleton(); }}
总结
本文主要总结了单例模式实现方式。两大类,7种实现。已经反射,序列化对单例的破坏。
静态内部类或者是枚举方式是最常见与官方推荐的实现方式。如果文章对您有些,帮助请关注下。
-
单例模式应用场景_Spring单例模式场景下,导致的诡异问题
2020-11-29 08:38:27首先更正一下,问题出现的原因不是spring单例模式有问题,而是对单例模式使用不当。下面我们来看看遇到了什么问题。今天业务功能代码提测,测试反馈有一个下拉框的列表出现诡异的情况,一会正序一会倒序,接到bug后... -
单例模式应用场景_【简易设计模式04】单例模式
2020-12-06 01:44:50总第56篇在上篇中,我们对整个系统的设计应遵循的六大设计原则进行了系统性的梳理和讲解。...1.单例模式的概念单例模式(Singleton Pattern)是一个比较简单的设计模式,它确保某一个类只有一个实例,并且自行实例化... -
单例模式应用场景_python单例模式的理解,就是单个实例
2020-12-10 20:19:11下面用普通实例和单例模式对比,来理解什么是单例模式和一些应用场景普通实例和单例模式区别普通实例:创建多个实例,执行多次__new__()方法,实例化创建多个,执行多次(默认)单例模式:创建多个实例,只执行一次__... -
设计模式之单例模式应用场景篇
2020-06-06 15:45:10应用场景 我们为什么要使用单例模式呢?它有什么好处? (一)单例模式可以让我们只创建一个对象从而避免了频繁创建对象导致的内存消耗和垃圾回收。 Servlet是单例模式,我们只需要创建一个Servlet,然后接收... -
java 单例应用场景_PHP单例模式应用场景有哪些?
2021-03-06 21:31:22有意义呀,比如你有个数据库对象 DB,非单例的做法就是每次用时$db = new DB();....$db->query("....");用单例,你只需要在工厂方法里判断是否已经初始化过了对象,有就返回,第一次就初始化,程序退出时断开(__... -
单例模式应用场景_尚学堂百战程序员之单例模式
2020-12-06 06:36:46应用程序的日志应用,一般都用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。 5. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是... -
Android单例模式应用场景
2015-02-16 23:59:09SQLITE数据库的使用是Android单例模式典型的应用场景。 单例模式好处: 将对象写成static,避免内存频繁实例化,因此对象在静态内存区只有一份。直接使用getInstance()取得对象。 模型类 ... -
单例模式应用场景_面试:请你谈谈单例模式的优缺点,注意事项,使用场景
2020-11-28 19:02:24单例模式介绍:单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的... -
单例模式应用场景_java 单例模式,还不懂?这一篇就够了
2020-12-06 06:36:41单例模式严格来讲有8种写法。利用类加载器帮助我们实现单例模式。用jvm来保证我们的线程安全。public class Mgr01{private static final Mgr01 INSTANCE=new Mgr01();private Mgr01();Public static Mgr01 ... -
单例模式应用场景_【0124期】请你谈谈单例模式的优缺点,注意事项,使用场景
2020-12-14 05:24:26单例模式介绍:单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的... -
单例模式应用场景_23种设计模式之------单例模式(不同应用场景下的实现方案)...
2020-12-10 20:19:09前言在Java的23种设计模式中,单例模式是最为简单,也是使用频率很高的一种设计模式,使用spring的同学都应该知道,spring类的默认加载方式就是单例的。所谓单例,也就是说这个类的实例对象只能有一个,如何达到这... -
设计模式-单例模式应用场景
2013-10-08 21:57:13好多没怎么使用过的人可能会想,单例模式感觉不怎么用到,实际的应用场景有哪些呢?以下,我将列出一些就在咱们周边和很有意义的单例应用场景。 1. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个... -
单例模式应用场景_【24期】请你谈谈单例模式的优缺点,注意事项,使用场景
2020-12-10 20:18:56点击上方“Java面试题精选”,关注公众号面试刷图,查缺补漏单例模式介绍:单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个... -
单例模式应用场景:
2014-08-13 11:11:47日志: 线程: 计数器: 数据库连接池:单例模式 -
单例模式应用场景_聊聊设计模式(3)
2020-12-10 20:19:10一、单例模式单例模式的定义与特点1 单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而... -
单例模式应用场景_介绍几个JavaScript设计模式及场景应用
2020-12-08 11:32:32作者 | 考拉海购前端团队链接 | https://juejin.im/post/59df4f74f265da430f311909设计模式的定义...因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模式。比如,当系统中某个... -
单例模式应用场景_设计模式大冒险第四关:单例模式,如何成为你的“唯一”...
2020-12-10 08:11:58今天这篇文章来跟大家一起学习一下单例模式。相信读完这篇文章之后,你肯定会有所收获的。关于单例模式,这应该是设计模式中最简单的一种了。大家如果学习过设计模式,可能很多设计模式长时间不用就忘记了,但是对于...