zoukankan      html  css  js  c++  java
  • 设计模式&单例模式

    一、首篇自序

    第一次整理博客,会稍微啰嗦讲几句,希望大家不要喷我^_^^_^,初来乍到不是很有经验。
    今天开始陆续会尽我一切的能力写一系列关于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设计模式之《深度分析代理模式》

        

        

  • 相关阅读:
    认识ASP.NET 中的 AppDomain
    试验总结1 改变递归函数中的执行内容
    试验总结2 break与continue
    开篇的话
    01复杂度3 二分查找
    02线性结构2 一元多项式的乘法与加法运算
    01复杂度2 Maximum Subsequence Sum
    02线性结构4 Pop Sequence
    01复杂度1 最大子列和问题
    02线性结构1 两个有序链表序列的合并
  • 原文地址:https://www.cnblogs.com/dszazhy/p/11510178.html
Copyright © 2011-2022 走看看