zoukankan      html  css  js  c++  java
  • 【pattern】设计模式(1)

    前言

      好久没写博客,强迫自己写一篇。只是总结一下自己学习的单例模式。

    说明

    单例模式的定义,摘自baike:

      单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。

      Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。

    懒加载(懒汉):

      只有主动调用实例化方法,才实例化单例的类。即,主动调用类似getInstance()返回实例对象。

    非懒加载(饿汉):

      主动调用实例化方法、或单例类被被动加载时,单例类就被实例化。

      (被动加载,假设情况A、B都是单例,且B继承A。现在主动实例化B,但由于类加载关系,会先实例化父类A。此时A类就是被动的被类加载器加载。)

    懒汉或饿汉的抉择:

      假设有一个[查询功能]单例类的实例化很占用内存空间[10M]、很消耗时间[10s]。

      如果是app饿汉,即启动app就实例该单例,那么会让启动app变慢[10s]、且多占用内存[10M]。这[10M]一直被浪费,直到用户使用到[查询功能]。所以建议用懒汉。

      如果是web饿汉,虽然会让web启动变慢[10s]、且[10M]内存长时间被浪费。但占用的是服务器资源,对用户来说,第一次使用[查询功能]是良好体验。因为不用花费[10s]去实例化。

        否则用户在第一次使用[查询功能]时,还要多等到[10s]去实例化单例。所以建议用饿汉。

      (只是简单举例说明,在spring、hibernate中有大量的懒加载机制。但理解不深)

    单例模式中的线程安全:

      指的是在任何情况下,不会因为多线程的关系造成单例类被多次实例化。而不是指单例类中的成员变量、或方法是线程安全的。

    关于网上介绍的单例模式的5种、或7种单例写法:

      在我看的几篇博客,其实都只能算5种。7种中有2对只是java上写法的不同,实质是一样的。

    建议单例写法:枚举单例模式 (但android可能要注意)。

      1、“使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。”

      2、android中枚举使用问题: “Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”

    枚举写法是不是懒加载:

      个人理解:枚举写法是懒加载

    image

      根据字节码可以看出,enum类是final类(不能被其他类继承)。所以不会存在被子类被动加载。

      enum类不会被反射、序列化构造对象。所以只有在主动调用时,enum类才被类加载器加载。

    GC会不会回收长时间不使用的单例对象:

      个人观点:不会

      除开枚举写法,所有单例类实例化后都是赋值给一个static的引用。e.g. private static Singleton instance = new Singleton();

      所以,在内存中一直存在一个引用instance指向单例的实例化对象new Singleton()。导致GC不会回收此单例类的实例化对象。

      然后对于枚举写法,根据枚举的特性,在被加载后是不会被GC回收的。(对枚举理解不深,所以不一定对。)

      详细可见: 单例模式讨论篇:单例模式与垃圾回收

    一、懒汉基本的单例模式(不建议这么写)
     /** 懒加载,线程不安全*/
       public class LazyInsec {
           private static LazyInsec instance;
    
           private LazyInsec(){}
    
           public static LazyInsec getInstance(){
               if(instance == null) instance = new LazyInsec();
               return instance;
          }
      }

    除非是单线程,不然不建议这么写。

    (懒汉:可以看出instance只有在主动调用getInstance()时才实例化。)

      1 /** 懒加载,线程安全。*/
      2 public class LazySec {
      3     private static LazySec instance;
      4     private LazySec(){}
      5     /**
      6      * 区别只在sync
      7      * @see LazySec#getInstance()
      8      */
      9     public static synchronized LazySec getInstance(){
     10         if(instance == null) instance = new LazySec();
     11         return instance;
     12     }
     13 
     14     /**
     15      * 线程知识。作用一样。
     16      * @see #getInstance()
     17      */
     18     public static LazySec getInstance2(){
     19         synchronized (LazySec.class){
     20             if(instance == null) instance = new LazySec();
     21             return instance;
     22         }
     23     }
     24 }

    不建议使用的原因是:同步影响并发性。且在绝大多数情况下是不需要这同步检测的。

    二、饿汉基本的单例模式
      1 /**
      2  * 单例模式03:积极加载(饿汉)、线程安全。<br/>
      3  */
      4 public class ActiveSec {
      5     /**
      6      * 饿汉:在类装载时就实例化。避免了多线程同步问题,所以线程安全。<br/>
      7      * 讨论:不能说积极加载比懒加载就差。积极加载只是在非必要的时候,就实例化了。
      8      */
      9     private static ActiveSec instance = new ActiveSec();
     10     private ActiveSec(){}
     11 
     12     public static ActiveSec getInstance(){
     13         return instance;
     14     }
     15 }
     16 
     17 /**
     18  * 单例模式04:饿汉变种、线程安全。<br/>
     19  * 网上也说了和{@link ActiveSec}差不多。但根据我对JVM的理解,其实只是写法不一样的区别。
     20  */
     21 class ActiveSec2{
     22     private static ActiveSec2 instance;
     23     static{
     24         instance = new ActiveSec2();
     25     }
     26     private ActiveSec2(){}
     27 
     28     public static ActiveSec2 getInstance(){
     29         return instance;
     30     }
     31 }

    饿汉:可以看出只要类被ClassLoader加载,那么就马上实例化。(static修饰符特性)

    其线程安全是通过类加载器ClassLoader的特性:同一个类,只能被相同的ClassLoader加载一次。

    三、静态内部类 - 懒汉
      1 /**
      2  * 单例模式05:懒加载、线程安全。<br/>
      3  * 区别03/04:前者是只要class类被装载,那么instance就赋值实例化对象(饿汉)。
      4  *  <br/>而此运用静态内部类特性,只有显示调过<code>getInstance()<code/>才会装载InnerClass,才会new LazySecInner();
      5  *  <br/>懒加载的目的:1.实例化相当消耗资源,让其在真正使用时才实例化。2.class可能被动的被装载,此时实例化是没意义的。
      6  *  <br/>
      7  */
      8 public class LazySecInner {
      9     private static class InnerClass{
     10         private static final LazySecInner INSTANCE = new LazySecInner();
     11     }
     12 
     13     private LazySecInner(){}
     14 
     15     public static final LazySecInner getInstance(){
     16         return InnerClass.INSTANCE;
     17     }
     18 }
     19 

    当LazySecInner被ClassLoader加载时,其内部类InnerClass并没有被ClassLoader加载。

    四、双重校验锁(JDK1.5后支持)

      1 /**
      2  * 单例模式06:双重校验锁。
      3  */
      4 public class DualSync {
      5     private volatile static DualSync instance;
      6 
      7     private DualSync(){}
      8 
      9     public static DualSync getInstance(){
     10         if(instance == null){
     11             synchronized (DualSync.class){
     12                 if(instance == null){
     13                     instance = new DualSync();
     14                 }
     15             }
     16         }
     17         return instance;
     18     }
     19 }

    写法太复杂,而且效率低下。虽然可能在jdk1.5+对多线程做了很多优化,但具体没了解。所以不怎么考虑这种写法。

    ****特别 为什么不建议使用以上写法

    1、可以通过反射得到新的实例化对象。

    public class Singleton {
        private static Singleton instance;
        private Singleton(){}
        public static Singleton getInstance(){
            if(instance == null) instance = new Singleton();
            return instance;
        }
    }
    
    class test{
        public static void main(String[] args) throws Exception {
            Singleton s1 = Singleton.getInstance();
            Singleton s2 = Singleton.getInstance();
    
            Class clazz = Class.forName(Singleton.class.getName());
            Constructor[] constructors = clazz.getDeclaredConstructors();
            constructors[0].setAccessible(true);
            Singleton s3 = (Singleton) constructors[0].newInstance();
    
            System.out.println(s1 == s2); // true
            System.out.println(s1 == s3); // false
        }
    }
    

    解决办法:

    image

    2、序列化和反序列化

    image

    这里测试是用的fastjson序列化。可以看出s1==s2、s1!=s3。所以通过反序列化能破坏单例。

    解决办法:

    image

    以上是针对fastjson的自定义序列化解析,如果是Serializable解决方案不一样。

    总之,要想办法自定义解析序列化。达到返回单例的目的。

    3、多个ClassLoader破坏单例

    摘自网上的解决方案。(暂时没用过多个ClassLoader,所以暂时不详细测试)

    private static Class getClass(String classname) throws ClassNotFoundException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        if(classLoader == null) classLoader = Singleton.class.getClassLoader();
    return (classLoader.loadClass(classname));
        }
    }

    JVM在搜索类的时候,又是如何判定两个class是相同的呢?

      JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。

      就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。

    详细浏览: 深入分析Java ClassLoader原理

    五、枚举单例模式(最推荐的写法
    /**
     * 单例模式06:利用枚举。<br/>
     * "这种方式是Effective Java作者Josh Bloch提倡的方式,避免多线程同步的同时,还能防止反序列化重新创建新的对象."
     */
    public enum LazySecEnum {
        INSTANCE;
        public void anyMethod(){
            //some code...
        }
    }

    1、枚举不能通过反射实例化,所以也不会存在被反射破坏。

    2、枚举不会被反序列化破坏。(枚举知识,不是很了解其原因) 参考:  深度分析 Java 的枚举类型:枚举的线程安全性及序列化问题

    3、(仅作参考)枚举会被多个ClassLoader破坏。(不太确定,没有测试过。且对JVM、ClassLoader理解也不深。所以这只做参考理解)

    对于android中枚举:

    Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android

    附录:

      你真的会写单例模式吗——Java实现 (重点最后的说明)

      Java:单例模式的七种写法 

      深入Java单例模式 

      单例模式讨论篇:单例模式与垃圾回收

      深度分析 Java 的枚举类型:枚举的线程安全性及序列化问题

       深入分析Java ClassLoader原理

  • 相关阅读:
    layer弹出层无法关闭问题
    layer iframe层ajax回调弹出layer.msg()
    layer iframe层弹出图片
    php部分基础
    小程序wx:key = “{{*this}}”报错
    运行jar包的命令
    spring aop
    Connection is read-only. Queries leading to data modification are not allowed
    操作录像命令----过程记录与回放
    开机自动登录图形化界面
  • 原文地址:https://www.cnblogs.com/VergiLyn/p/6218992.html
Copyright © 2011-2022 走看看