各位老铁们,大家好,今天由我来为大家分享收录啦网站源码分享,以及网站收录是什么意思的相关问题知识,希望对大家有所帮助。如果可以帮助到大家,还望关注收藏下本站,您的支持是我们最大的动力,谢谢大家了哈,下面我们开始吧!
前言
欢迎最美最帅的你点赞哦~~~!
单例模式是面向对象的编程语言23种设计模式之一,属于创建型设计模式。主要用于解决对象的频繁创建与销毁问题,因为单例模式保证一个内仅会有一个实例。大部分对单例模式应该都知道一些,但面试的时候可能回答不会很完整,不能给自己加分,甚至扣分。
单一的知识点并不能对自己在面试的时候带来加分,而系统的知识则会让面试官另眼相看,而本文会系统的介绍单例模式的基础版本与完美版本,基本上将单例模式的内容完全包括。如果认为有不同的意见可以留言交流。
源码已收录github查看源码
单例模式最重要的就是保证一个类只会出现一个实例,那么超过一个就不能被称为是单例,所以其代码构成如下特点。
私有化构造器,禁止从外部创建单例对象。提供一个全局的访问点获取单例对象。
什么是全局访问点?好吧,上面的话语太文邹邹了,如果我说公共的静态方法呢?
饿汉、懒汉
主要分为饿汉模式和懒汉模式。那何为饿汉?何为懒汉?
小丽的爸爸从小生活很艰苦,经历了饥荒年代,所以对食物非常紧张。当小丽去上学的时候,不管小丽是否需要,都会给小丽准备很多的零食。
而小明的爸爸则是一个非常懒惰的人,所有的事情都会到最后才去做,所有事情只有当有别人来叫他的时候,他才会把事情做完这样就引出了我们对饿汉模式和懒汉模式的定义:
饿汉模式:不管单例对象是否被使用,都会先创建出一个对象。懒汉模式存在资源浪费的问题,因为很有可能对象创建出来只会永远都不会被使用到。
代码如下:
packagedemo.single;\n/**\n*饿汉模式\n*/\npublicclassHungrySingle{\n/**\n*饿汉模式,不管hungrySingle对象是否有使用到,都会先创建出来\n*由于饿汉模式在对象使用之前就已经被创建,所以是不会存在线程安全问题\n*/\nprivatestaticHungrySinglehungrySingle=newHungrySingle();\n/**\n*私有化构造器,禁止外部创建\n*/\nprivateHungrySingle(){\n}\n/**\n*提供获取实例的方法\n*/\npublicstaticHungrySinglegetInstance(){\nreturnhungrySingle;\n}\n}\n复制代码
懒汉模式:不会先将对象创建出来,而是等到有人使用的时候才会创建。相比饿汉模式,懒汉模式不会存在资源浪费的情况,所以基本都会选择懒汉模式。
代码如下:
packagedemo.single;\n/**\n*懒汉模式\n*/\npublicclassLazySingle{\n/**\n*懒汉模式,不会先创建对象,而是在调用的时候才会创建对象\n*/\nprivatestaticLazySinglelazySingle=null;\nprivateLazySingle(){\n}\n/**\n*调用的时候创建对象并返回\n*/\npublicstaticLazySinglegetInstance(){\nif(lazySingle==null){\nlazySingle=newLazySingle();\n}\nreturnlazySingle;\n}\n}\n复制代码
小李:面试官,您看我这样的解释可还行。
面试官:单线程下是挺好的,如果在多线程环境下呢?
小李:这个我知道,加锁啊!
面试官:出门左转电梯直达!
其实加锁也没答错,关键问题在于如何加锁!
直接将获取实例的方法内容写入同步代码块中,解决了多线程安全的问题,但是并发效率的问题又暴露了出来。你想啊,现在锁住了这方法,而无论单例的对象是否创建,都会经过获取锁、释放锁的过程。这样的性能显然是不能接受的。
小李:我想想啊~~~!Emmmmm…!有了,我们可以在同步代码块外增加一个判断,如果对象已经创建则直接返回。
面试官:这样解决了一部分的并发效率问题,但是如果在创建的时候同时有很多的线程访问,是不是也会有并发的效率问题呢?再优化优化。
小李一想,确实是这样,如果对象还没有创建出来的时候,就有很多的线程来访问,也会出现问题,假设有两个线程同时访问,当A线程优先争抢到锁,A进入同步代码块执行,此时B没有争抢到锁,将处于等待状态,而当A线程执行完成后释放锁,B进入同步代码块执行,此时B线程同样会创建出一个对象,破坏了单例。
小李:面试官,我明白了,可以在同步代码块中再加一层if判断,如果对象已经创建,就直接返回即可。
DoubleCheck
上面最后的结果就是我们常说的DoubleCheck,即双重锁检查。双重锁检查在很多地方都被运用到,代码如下。
packagedemo.single;\n/**\n*懒汉模式\n*/\npublicclassLazySingle{\n/**\n*懒汉模式,不会先创建对象,而是在调用的时候才会创建对象\n*/\nprivatestaticLazySinglelazySingle=null;\nprivateLazySingle(){\n}\n/**\n*调用的时候创建对象并返回\n*/\npublicstaticLazySinglegetInstance(){\n//firstcheck\nif(lazySingle!=null){\nsynchronized(LazySingle.class){\n//doublecheck\nif(lazySingle==null){\nlazySingle=newLazySingle();\n}\n}\n}\nreturnlazySingle;\n}\n}\n复制代码
面试官:小李,你多线程运行一下代码看看呢。
小李:好勒!好像挺正常啊。等等,好像不对,这里还是出现了多个对象!!!啊~~,这是为什么啊,我都懵了,这完全超出了我的能力范围。
面试官:哈哈,小子,这下知道谁是大佬了吧?我来给你好好解释一下,其实,这和我们的代码没有关系,正常来讲,应该不会出现这样的问题,但是我们都知道,代码在运行过程中,会被编译成一条一条的指令运行,而JVM在运行时,在保证单线程最终结果不会受影响的情况下,对指令进行优化,就有可能对指令进行重排序,同样会破坏单例。
lazySingle=newLazySingle();\n//这样一段代码在运行时会生成3条指令,即:1.分配内存空间2.创建对象3.指向引用\n//正常情况下是会按照123顺序执行,但JVM优化器进行指令重排后,则可能变为:1.分配内存空间3.指向引用2.创建对象\n//在单线程下,这样的优化没有问题,但是多线程下,线程是在争抢CPU时间碎片的。假设A刚刚执行完13//条指令,此时B争抢到时间碎片,发现对象不为空了,就直接返回,但此时对象还没有真正被创建。B调用\n//此对象就会抛出异常\n//而volatile关键字修饰的变量可以禁止指令重排序,则可以保证指令会是123顺序执行。\n//加上volatile修饰\nprivatevolatilestaticLazySinglelazySingle=null;\n复制代码
小李:终于解决了,好难啊,一个简单的单例模式居然有这么多的细节。
面试官:你以为这就完了?
内部类的单例
使用内部类的方式可以非常完美的完成单例模式,而实现代码也非常简单。
packagedemo.single;\n\n/**\n*内部类的方式实现单例\n*/\npublicclassInnerSingle{\n/**\n*私有化构造器\n*/\nprivateInnerSingle(){\n}\n/**\n*私有内部类\n*/\nprivatestaticclassInner{\n//Jingtai内部类持有外部类的对象\npublicstaticfinalInnerSingleSINGLE=newInnerSingle();\n}\n/**\n*返回静态内部类持有的对象\n*/\npublicstaticInnerSinglegetInstance(){\nreturnInner.SINGLE;\n}\n}\n\n复制代码
可以看到,代码中并没有出现同步方法或者同步代码块,那么静态内部类的方式是如何做到安全的单例模式呢?
外部类加载的时候,不会立即加载内部类,而是在调用的时候会加载内部类。不管多少线程访问,JVM一定会保证类被正确的初始化,即静态内部类的方式是在JVM层面保证了线程安全
当然,这样也有一些缺点,那就是在创建单例对象的时候,如果需要传参,那么静态内部类的方式会非常麻烦。
破坏单例
那么,上面的单例已经完美了吗?并没有,看我如何将单例给破坏掉。
反射破坏
反射可以绕过私有构造器的限制,创建对象。当然正常的调用是不会发生单例被破坏的情况,但是如果偏偏有人不走寻常路呢,比如下面的调用。
packagedemo.single;\n\nimportjava.lang.reflect.Constructor;\n/**\n*反射破坏单例\n*/\npublicclassRefBreakSingleTest{\npublicstaticvoidmain(String[]args)throwsException{\n//获取类对象\nClass<LazySingle>lazySingleClass=LazySingle.class;\n//获取构造器\nConstructor<LazySingle>constructor=lazySingleClass.getDeclaredConstructor(null);\nconstructor.setAccessible(true);\n//创建对象\nLazySinglelazySingle=constructor.newInstance(null);\nSystem.out.println(lazySingle);\nSystem.out.println(LazySingle.getInstance());\nSystem.out.println(lazySingle==LazySingle.getInstance());\n}\n}\n复制代码
测试结果
很明显看到出现了两个不同的兑现,显然,单例被破坏了!对于这样的情况该如何禁止呢?在网上查阅了很多资料,大部分是使用变量控制法,即在类中添加一个变量用于判断单例类的构造器是否有被调用,代码如下。
//添加变量控制,防止反射破坏\nprivatestaticbooleanisInstance=false;\nprivatevolatilestaticLazySinglelazySingle=null;\nprivateLazySingle()throwsException{\nif(isInstance){\nthrownewException(&34;);\n}\nisInstance=true;\n}\n复制代码
再次调用测试代码,发现不能再创建多个单例对象,程序抛出了异常。
但是别忘了,属性也是可以通过反射修改的(count、instance的判断反射都能绕过)。
publicclassRefBreakSingleTest{\npublicstaticvoidmain(String[]args)throwsException{\n//获取类对象\nClass<LazySingle>lazySingleClass=LazySingle.class;\n\n//获取构造器\nConstructor<LazySingle>constructor=lazySingleClass.getDeclaredConstructor(null);\nconstructor.setAccessible(true);\n//创建对象\nLazySinglelazySingle=constructor.newInstance(null);\nSystem.out.println(lazySingle);\nFieldisInstance=lazySingleClass.getDeclaredField(&34;);\nisInstance.setAccessible(true);\nisInstance.set(null,false);\nSystem.out.println(LazySingle.getInstance());\nSystem.out.println(lazySingle==LazySingle.getInstance());\n}\n}\n复制代码
单例再次被破坏,感觉是不是已经快崩溃了,一个单例咋这么多事呢!!既然私有属性、私有方法在外部都能通过反射获取,那有没有反射不能获取的呢?我在网上也找到了另外一种写法,即私有内部类的来持有实例控制变量,而我也通过测试,发现反射同样能够绕过从而破坏单例。
packagedemo.pattren.single;\n\nimportjava.lang.reflect.Constructor;\nimportjava.lang.reflect.Method;\n\npublicclassBreakInnerTest{\npublicstaticvoidmain(String[]args)throwsException{\nClass<LazySingle>lazySingleClass=LazySingle.class;\n////获取构造器\nConstructor<LazySingle>constructor=lazySingleClass.getDeclaredConstructor(null);\nconstructor.setAccessible(true);\n//创建对象\nLazySinglelazySingle=constructor.newInstance(null);\n//获取内部类的类对象\nClass<?>aClass=Class.forName(&34;);\nMethod[]methods=aClass.getMethods();\nConstructor<?>[]declaredConstructors=aClass.getDeclaredConstructors();\nSystem.out.println(declaredConstructors);\nConstructor<?>declaredConstructor=declaredConstructors[0];\ndeclaredConstructor.setAccessible(true);\n//创建内部类需要传入一个外部类的对象\nObjecto=declaredConstructor.newInstance(lazySingle);\n//成功绕过\nmethods[0].invoke(o);\n}\n}\n复制代码
目前网上基本都是这两种,但是反射都是能够绕过判断进行破坏。可以这样认为,这种方式反射是可以破坏的,不能100%保证单例不被破坏。欢迎各位提供完美的示例。
序列化破坏
Java的IO提供了对象流,用来将对象写入磁盘、从磁盘读取对象的功能。这也成为了单例的破坏点。
publicstaticvoidmain(String[]args)throwsException{\n//正常的方式获取单例对象\nInnerSingleinstance=InnerSingle.getInstance();\n\n//写入磁盘\nFileOutputStreamfos=newFileOutputStream(&34;);\nObjectOutputStreamoos=newObjectOutputStream(fos);\noos.writeObject(instance);\noos.close();\nfos.close();\n\n//从磁盘读取对象\nFileInputStreamfis=newFileInputStream(&34;);\nObjectInputStreamois=newObjectInputStream(fis);\nInnerSingleinnerSingle=(InnerSingle)ois.readObject();\n\nSystem.out.println(instance);\nSystem.out.println(innerSingle);\nSystem.out.println(innerSingle==instance);\n}\n复制代码
而序列化的方式JVM提供了一种机制,可以防止单例被破坏,即在单例类中添加readResovle方法。
//在反序列化时,readResolve方法,则直接返回该方法指定的对象\nprivateObjectreadResolve(){\nreturngetInstance();\n}\n复制代码
测试结果:
序列化没有再破坏单例,而这一切JDK是如何处理的呢?
publicfinalObjectreadObject()\nthrowsIOException,ClassNotFoundException\n{\nif(enableOverride){\nreturnreadObjectOverride();\n}\nintouterHandle=passHandle;\ntry{\n//关键代码,最终返回的是此方法返回的对象\nObjectobj=readObject0(false);\nhandles.markDependency(outerHandle,passHandle);\nClassNotFoundExceptionex=handles.lookupException(passHandle);\n//morecodebutnotimportent\n复制代码
继续深入,发现readObject0方法的关键代码如下
bytetc;\n//取出文件的一个字节,判断读取的对象类型\nwhile((tc=bin.peekByte())==TC_RESET){\nbin.readByte();\nhandleReset();\n}\ndepth++;\ntotalObjectRefs++;\ntry{\nswitch(tc){\ncaseTC_NULL:\nreturnreadNull();\ncaseTC_ENUM:\nreturncheckResolve(readEnum(unshared));\n//判断为对象类\ncaseTC_OBJECT:\nreturncheckResolve(readOrdinaryObject(unshared));\n//moreothrercase\n复制代码
继续追踪readOrdinaryObject方法,发现readReslove的关键代码
//判断是否有readReslove方法(desc.hasReadResolveMethod())\nif(obj!=null&&\nhandles.lookupException(passHandle)==null&&\ndesc.hasReadResolveMethod())\n{\n//执行readReslove\nObjectrep=desc.invokeReadResolve(obj);\nif(unshared&&rep.getClass().isArray()){\nrep=cloneArray(rep);\n}\nif(rep!=obj){\n//Filterthereplacementobject\nif(rep!=null){\nif(rep.getClass().isArray()){\nfilterCheck(rep.getClass(),Array.getLength(rep));\n}else{\nfilterCheck(rep.getClass(),-1);\n}\n}\n//最终返回readReslove方法的执行结果\nhandles.setObject(passHandle,obj=rep);\n}\n}\nreturnobj;\n复制代码
枚举单例-最完美的单例模式
大神JoshBloch在《EffectiveJava》中极力推荐使用枚举的方式来实现单例。
packagedemo.single;\n\npublicenumEnumSingle{\nSINGLE;\npublicvoiddoJob(){\nSystem.out.println(&34;);\n}\n}\n复制代码
枚举类型是单例模式的最佳选择,主要得益于JVM对于枚举类型的支持:
JVM保证枚举类型的每个实例仅存在一份枚举类型的序列化与反序列化不会破坏其单例的特性(上面的源码大家可以去找一找)反射也不能破坏枚举单例
可以说,枚举天然就是单例的,那么你会选择枚举作为单例吗?
OK,关于收录啦网站源码分享和网站收录是什么意思的内容到此结束了,希望对大家有所帮助。
