zoukankan      html  css  js  c++  java
  • 设计模式之美—单例模式

    单例模式

    什么是单例?

    1. 该类只能有一个实例。
    2. 该类负责创建自己的对象。
    3. 在整个项目中都能访问到这个实例。

    应用场景

    1. 读配置文件时,维护全局的Config类。
    2. 线程池、连接池,统一进行分配和管理。
    3. 网站的计数器,也可以采用单例模式实现,保持同步

    代码实现

    饿汉式

       中国古代神话中有女娲补天一说,现在天破了,我们去求女娲补天。

      女娲用英语来说是 A Goddess In Chinese Mythology,意思就是神话中的女神,女娲是独一无二的,现在我们就建一个女神类Goddess。

    1 public class Goddess {
    2     
    3 }

      神话中,我们都是女娲造出来的,人是不能造女娲的,所以要女娲私有化构造。

    1 public class Goddess {
    2     private Goddess(){};//私有化构造
    3 }

      既然人不能女娲,那女娲是怎么来的,女娲伴随天地初开产生的,所以要自己创建对象

    1 public class Goddess {
    2     private static final Goddess goddess = new Goddess();//自己创建对象
    3     private Goddess(){};//私有化构造
    4 }

      女娲是神秘的,凡胎肉眼看不到所以要private,static保证了女娲伴随天地初开,在内存中永生,不能被垃圾回收器回收。final保证女娲是不会变的,永远是那个女神,啧...啧...

    1 public class Goddess {
    2     private static final Goddess goddess = new Goddess();//自己创建对象
    3     private Goddess(){};//私有化构造
    4     public static Goddess getInstance(){//获取唯一可用的对象
    5         return goddess;
    6     }
    7 }

      既然单例不能被实例化,就需要一个静态的方法来获取对象。这是单例的“饿汉式”,代码第二行就产生了女娲。

      我们来验证一下:

    1 public class GoddessTest {
    2     public static void main(String[] args){
    3         Goddess goddes1 = Goddess.getInstance();
    4         Goddess goddes2 = Goddess.getInstance();
    5         System.out.println("两个对象的引用是否相等:"+(goddes1==goddes2));
    6     }
    7 }

      结果:

    两个对象的引用是否相等:true

      两个对象的引用是同一个对象,说明我们实现了单例模式。

    懒汉式

     1 public class Goddess {
     2     private static Goddess goddess ;//此时不创建对象
     3     private Goddess(){};//私有化构造
     4     public static Goddess getInstance(){//获取唯一可用的对象
     5 
     6         if (goddess == null) {//如果没有女娲才去创建
     7             goddess = new Goddess();
     8         }
     9         return goddess;
    10     }
    11 }

      女娲比较懒,没事干的时候,处于混沌状态,第一次求女娲时,女娲才会实例化。

      好处:省了一段时间的内存,

      坏处:第一次请女娲会慢,因为要消耗CPU去实例化。多线程下也有问题,如果多个人同时请女娲,会产生很多女娲,不是线程安全。

      我们加上synchronized进行优化,线程调用前必须获取同步锁,调用完后会释放锁给其他线程用,也就是请女娲必须排队,大家一个一个来。

     1 public class Goddess {
     2     private static Goddess goddess ;//此时不创建对象
     3     private Goddess(){};//私有化构造
     4     public static synchronized Goddess getInstance(){//获取唯一可用的对象
     5 
     6         if (goddess == null) {//如果没有女娲才去创建
     7             goddess = new Goddess();
     8         }
     9         return goddess;
    10     }
    11 }

      然而,这样多个人都要去抢锁,即使对象已经实例化,没有充分利用CPU资源并发优势(特别是多核情况)。

      我们把synchronized放到方法体内,如果女娲还没实例化,才回去抢锁,这就极大的利用了CPU资源。

      代码如下:

    双检锁/双重校验锁

     1 //这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
     2 public class Goddess {
     3     private volatile static Goddess goddess ;//此时不创建对象
     4     private Goddess(){};//私有化构造
     5     public static Goddess getInstance(){//获取唯一可用的对象
     6         if (goddess == null) {//如果女娲还没有实例化,请女娲的人进行等待,准备抢夺请求锁。
     7             synchronized(Goddess.class){
     8                 if (goddess == null) {//第一个抢过之后,后面的人也不用等待了,女娲还有五秒到达战场
     9                     goddess = new Goddess();
    10                 }
    11             }
    12         }
    13         return goddess;
    14     }
    15 }

     枚举模式

    1 public enum Goddess {
    2     INSTANCE;
    3     public Goddess getInstance(){
    4         return INSTANCE;
    5     }
    6 }

      以上方式是在不考虑反射和序列化的情况下实现的,如果考虑了反射,就无法做到单例类只能有一个实例这种说法了。但是枚举单例模式可以避免这两种情况。

    我们以饿汉式为例:

     1 public class GoddessTest {
     2     public static void main(String[] args) throws Exception {
     3         Goddess goddes1=Goddess.getInstance();
     4         Goddess goddes2=Goddess.getInstance();
     5         Constructor<Goddess> constructor=Goddess.class.getDeclaredConstructor();
     6         constructor.setAccessible(true);
     7         Goddess goddes3=constructor.newInstance();
     8         System.out.println("正常情况下,两个对象的引用是否相等:"+(goddes1 == goddes2));
     9         System.out.println("反射攻击时,两个对象的引用是否相等:"+(goddes1 == goddes3));
    10     }
    11 }

      结果:

    1 正常情况下,两个对象的引用是否相等:true
    2 反射攻击时,两个对象的引用是否相等:false

      枚举单例下示例:

     1 public class GoddessTest {
     2     public static void main(String[] args) throws Exception {
     3         Goddess goddes1=Goddess.INSTANCE;
     4         Goddess goddes2=Goddess.INSTANCE;
     5         Constructor<Goddess> constructor= null;
     6         constructor = Goddess.class.getDeclaredConstructor();
     7         constructor.setAccessible(true);
     8         Goddess goddes3= null;
     9         goddes3 = constructor.newInstance();
    10         System.out.println("正常情况下,两个对象的引用是否相等:"+(goddes1 == goddes2));
    11         System.out.println("反射攻击时,两个对象的引用是否相等:"+(goddes1 == goddes3));
    12     }
    13 }

      结果:

    1 Exception in thread "main" java.lang.NoSuchMethodException: xxx.xxx.Goddess.<init>()
    2     at java.lang.Class.getConstructor0(Class.java:3082)
    3     at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    4     at com.slw.design.danli.GoddessTest.main(GoddessTest.java:11)

      反射通过newInstance创建对象时,会检查该类是否enum修饰,如果是则抛出异常,反射失败。所以枚举是不怕发射攻击的。

  • 相关阅读:
    面向对象(metaclass继承高级用法)
    建表和删表(sqlalchemy框架)
    单表操作
    认证,权限
    协程,twisted
    定制起始url(scrapy_redis)
    浅谈深度优先和广度优先(scrapy-redis)
    scrapy-redis(调度器Scheduler源码分析)
    scrapy-redis
    xpath
  • 原文地址:https://www.cnblogs.com/yeshensi/p/11671905.html
Copyright © 2011-2022 走看看