设计模式&单例模式

一、首篇自序

第一次整理博客,会稍微啰嗦讲几句,希望大家不要喷我^_^^_^,初来乍到不是很有经验。
今天开始陆续会尽我一切的能力写一系列关于java文章内容,要是哪里做的不好,还希望大家多多指教。
我会接受大家的意见,不断进步。
当然也希望我的文章能够帮助到大家,哪怕是一点点也好。
尤其是java需要循序渐进的去学习,去进步,一口吃成胖子是很难。
如果在坐的大家有想学习java,或者正在初学java的小伙伴,请不要着急。
个人觉得要学好java最好的方式就是:首先要舍得付出,其次每天要求自己学习2小时计划,再就是要合理的回顾以及练习。
这样坚持下去,走好每一天,不要烦躁这个想学那个想学。最后时间走了,学到的东西都是半斤八两。
当然这只是我个人的意见,大家有什么好的学习建议也可以分享分享。
最后废话不多说了,今天开始给大家总结一下设计模式中的《单例模式》。
后续的博客规划:设计模式、并发编程、Spring核心原理、Mybatis源码分析、IO/Netty、SpringBoot、SpringCloud、Apache Dubbo、Zookeeper、Nacos、Sentinel,kafka原理分析、RabbitMQ原理分析、RocketMQ原理分析、docker、k8s 。。。。。。

二、单例设计模式讲解内容(由于最近工作也是比较饱和时间有限,后续会将文章相关源码托管到git上、请大家原谅)

 1、单例模式模式的应用场景

      单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并 提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 常见的如:ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接 池也都是单例形式。

 2、常见的单例模式写法以及如何在多线程下保证单例的安全

      a、饿汉式单例

       饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。

       优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。

       缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。

       饿汉单例实现一:

      public class DszSingleton {

             //先静态、后动态

            //先属性、后方法

            //先上后下

            private static final DszSingleton dszSingleton = new DszSingleton ();

            private DszSingleton (){    

                //即使构造私有了,但是还是要防止反射强吻攻击
if(null != dszSingleton ){
throw new RuntimeException("不要乱搞,别以为不知道你在用反射");
}

            }

           public static DszSingleton getInstance(){

                   return DszSingleton ;

            }

      }

    饿汉单例(静态代码加载机制)实现二:

      public class DszSingleton {

                 private static final DszSingleton dszSingleton ;

                 static { //由此加载

                               dszSingleton = new DszStaticSingleton();

                  }

                 private DszStaticSingleton(){

                 //即使构造私有了,但是还是要防止反射强吻攻击
if(null != dszSingleton ){
throw new RuntimeException("不要乱搞,别以为不知道你在用反射");
}

                 }

                 public static DszStaticSingleton getInstance(){

                             return dszSingleton;

                 }

      }

      b、懒汉式单例

           懒汉式单例的特点是:被外部类调用的时候内部类才会加载,下面看懒汉式单例的简单

           懒汉单例实现一:

           //懒汉式单例 //在外部需要使用的时候才进行实例化

          public class LazySimpleSingleton {

                                   //静态块,公共内存区域

                                   private static LazySimpleSingleton lazy = null;

                                   private LazySimpleSingleton(){   

                       //即使构造私有了,但是还是要防止反射强吻攻击
if(null != lazy){
throw new RuntimeException("不要乱搞,别以为不知道你在用反射");
}

                                    }

                                   public static LazySimpleSingleton getInstance(){

                                            if(lazy == null){ //当然这里在多线程下是不安全的,下面会给大家展示这种解决方案

                                                     lazy = new LazySimpleSingleton();

                                             }

                                          return lazy;

                                       }

           }

           懒汉单例(优化)实现二:

           public class LazyDoubleCheckSingleton {

                                private volatile static LazyDoubleCheckSingleton lazy = null;

                                private LazyDoubleCheckSingleton(){        

                       //即使构造私有了,但是还是要防止反射强吻攻击
if(null != lazy){
throw new RuntimeException("不要乱搞,别以为不知道你在用反射");
}

                                }

                                public static LazyDoubleCheckSingleton getInstance(){

                                        if(lazy == null){//第一层判定

                                                   synchronized (LazyDoubleCheckSingleton.class){

                                                              if(lazy == null){//第二层判定

                                                                                   lazy = new LazyDoubleCheckSingleton();

                                                                }

                                                    }

                                              }

                                         return lazy;

                                         }

                                  }

           c、注册式单例

             注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标 识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。

             ->枚举式单例                    

               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;
                            }
                  }

             //------MainTest------ 

public static void main(String[] args) {
try {
EnumSingleton instance1 = null;

EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());

FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();

FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1 = (EnumSingleton) ois.readObject();
ois.close();

System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData() == instance2.getData());

}catch (Exception e){
e.printStackTrace();
}
}
运行结果:

    

  为什么枚举可以保证单例的安全?让我们来分析一下原理,这里大家需要安装一下Java 反编译工具 Jad(下载地址:https://varaneckas.com/jad/)

   找到工程所 在的 class 目录,复制 EnumSingleton.class

 

 然后切回到命令行,切换到工程所在的 Class 目录,输入命令 jad EnumSingleton.class,我们会在 Class 目录下会多一个 EnumSingleton.jad 文件。

打开 EnumSingleton.jad 文件我们惊奇又巧妙地发现有如下代码:

    

 原来,枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现。

所以序列化和反射都是无法破坏枚举生成的单例,这里序列化不能破坏枚举单例就不分析了,其实跟下面讲的序列化readResolve类似场景,大家可以仿照去分析一下。

这里来分析一下反射为什么不能破坏枚举单例原理:

 测试代码会报异常:

       

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("duasnhouzhi ",666);

}catch (Exception e){
e.printStackTrace();
}
}


这时错误已经非常明显了,告诉我们 Cannot reflectively create enum objects,不能 用反射来创建枚举类型。还是习惯性地想来看看 JDK 源码,进入 Constructor 的 newInstance()方法:

@CallerSensitive
public T newInstance(Object... var1) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (!this.override && !Reflection.quickCheckMemberAccess(this.clazz, this.modifiers)) {
Class var2 = Reflection.getCallerClass();
this.checkAccess(var2, this.clazz, (Object)null, this.modifiers);
}

if ((this.clazz.getModifiers() & 16384) != 0) {
//这里意思是在 newInstance()方法中做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型, 直接抛出异常。
//这里就给大家分析了为什么java发序列化为什么不能破坏枚举单例
throw new IllegalArgumentException("Cannot reflectively create enum objects");
} else {
ConstructorAccessor var4 = this.constructorAccessor;
if (var4 == null) {
var4 = this.acquireConstructorAccessor();
}

Object var3 = var4.newInstance(var1);
return var3;
}
}

    ->容器缓存式单例

//Spring中的做法,就是用这种注册式单例
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getInstance(String className){
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}

容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。

   d、利用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();
}
}

   这里简单讲一下ThreadLocal为什么实现单例,这是根据他的特点决定的,它是一个独占线程安全工具。具体大家可以先去了解一下,这里就不做具体分析了。

 e、内部类实现单例

//这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
//完美地屏蔽了这两个缺点
public class LazyInnerClassSingleton {
//默认使用LazyInnerClassGeneral的时候,会先初始化内部类
//如果没使用的话,内部类是不加载的
private LazyInnerClassSingleton(){
if(LazyHolder.LAZY != null){
throw new RuntimeException("不允许创建多个实例");
}
}

//每一个关键字都不是多余的
//static 是为了使单例的空间共享
//保证这个方法不会被重写,重载
public static final LazyInnerClassSingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}

//默认不加载
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
内部类单例调用示例:
public class LazyInnerClassSingletonTest {

public static void main(String[] args) {
try{
//很无聊的情况下,进行破坏
Class<?> clazz = LazyInnerClassSingleton.class;

//通过反射拿到私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//强制访问
c.setAccessible(true);
//暴力初始化
Object o1 = c.newInstance();
//调用了两次构造方法,相当于new了两次
//犯了原则性问题,
Object o2 = c.newInstance();
System.out.println(o1 == o2);
}catch (Exception e){
e.printStackTrace();
}
}
}

 3、反射破坏单例解决方案及原理分析

     public class LazyInnerClassSingletonTest {
                              public static void main(String[] args) {
                                                     try{
                                                                Class<?> clazz = LazyInnerClassSingleton.class;
                                                                //通过反射拿到私有的构造方法
                                                                Constructor c = clazz.getDeclaredConstructor(null);
                                                                //强制访问,设置为true反射就可以强制访问构造方法了
                                                                c.setAccessible(true);
                                                                //暴力反射调用构造,初始化单例对象
                                                                Object o1 = c.newInstance();
                                                                //调用了两次构造方法,相当于 new 了两次
                                                                Object o2 = c.newInstance();

                                                                //比较两次出来的对象是否内存地址一致,显然是不等的。false
                                                                System.out.println(o1 == o2);
                                                           }catch (Exception e){ 
                                                                      e.printStackTrace();
                                                           }
             }

           

          注:if(null != lazy){ throw new RuntimeException("不要乱搞,别以为不知道你在用反射");}这段代码就是为了防止以上反射去破坏单例环境

 4、序列化破坏单例的原理及解决方案

       import java.io.Serializable;
       public class SeriableSingleton implements Serializable {
                public final static SeriableSingleton INSTANCE = new SeriableSingleton();
                private SeriableSingleton(){}
                public static SeriableSingleton getInstance(){
                       return INSTANCE;
                }

               //以下代码为了防止序列化破坏单例(去掉一下代码序列化就可以成功破坏单例环境),为什么?我们接下来一起分析一下
               private Object readResolve(){
                     return INSTANCE;
               }
       }

     序列化破坏原理分析

          首先看一下序列化破坏的java实现代码

          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);//A代码
                                                   oos.flush();
                                                   oos.close();
                                                   FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
                                                   ObjectInputStream ois = new ObjectInputStream(fis);
                                                   s1 = (SeriableSingleton)ois.readObject();//B代码
                                                   ois.close();
                                                  System.out.println(s1);
                                                  System.out.println(s2);
                                                  System.out.println(s1 == s2);
                                        } catch (Exception e) {
                                                  e.printStackTrace();
                                        }
                                   }
                             }

               以上是序列化破坏单例的实例教程,调用结果 false

               运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两 次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其 实很简单,只需要增加 readResolve()方法即可。

               我 们 一 起 来 看 看 JDK 的 源 码 实 现 以 一 清 二 楚 了 。 我 们 进 入 ObjectInputStream 类的 readObject()方法,代码如下:

    //一下是jdk源码  

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 {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}

depth++;
totalObjectRefs++;
try {
switch (tc) {
......
//这里就是为什么反序列可以控制单例,只要实现readResolve()方法
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
......

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
我们看到 TC_OBJECTD 中判断,调用了 ObjectInputStream 的 readOrdinaryObject() 方法,我们继续进入看源码: private Object readOrdinaryObject(boolean
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;

                 

                  代码非常简单,就是判断一下构造方法是否为空,构造方法不为空就返回 true。意味着只要有无参构造方法就会实例化。

                  这时候,其实还没有找到为什么加上 readResolve()方法就避免了单例被破坏的真正原因。

    } catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}

。。。。。。

return obj;
}

我再回到 ObjectInputStream 的 readOrdinaryObject()方法继续往下看:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
。。。。。。

if (obj != null &&
handles.lookupException(passHandle) == null &&
//判断无参构造方法是否存在之后,又调用了 hasReadResolveMethod()方法,来看代码:
desc.hasReadResolveMethod())
进入代码看看如下:

逻辑非常简单,就是判断 readResolveMethod 是否为空,不为空就返回 true。那么 readResolveMethod 是在哪里赋值的呢?通过全局查找找到了赋值代码在私有方法 ObjectStreamClass()方法中给 readResolveMethod 进行赋值,来看代码:

上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在 再 回 到 ObjectInputStream 的 readOrdinaryObject() 方 法 继 续 往 下 看 , 如 果 readResolve()存在则调用 invokeReadResolve()方法,来看代码:

                 我们可以看到在 invokeReadResolve()方法中用反射调用了 readResolveMethod 方法。

                 通过 JDK 源码分析我们可以看出,虽然,增加 readResolve()方法返回实例,解决了单 例被破坏的问题。

                 但是,我们通过分析源码以及调试,我们可以看到实际上实例化了两 次,只不过新创建的对象没有被返回而已。

                 那如果,创建对象的动作发生频率增大,就 意味着内存分配开销也就随之增大

    {
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
。。。。。。

return obj;
}

 5、单例总结

    单例的实现几种形式:饿汉式、懒汉式、内部类、枚举、容器、ThreadLocal等六种形式(当然还有其他形式大家可以自行了解)。

    破坏单例的常见方式:序列化、java反射。掌握以上哪些单例形式可以被反射或序列化破坏,以及如何才能防止这两种方式的破坏原理。

6、下次文章规划讲解内容

     java设计模式之《深度分析代理模式》

    

    

原文地址:https://www.cnblogs.com/dszazhy/p/11510178.html