zoukankan      html  css  js  c++  java
  • GOF23设计模式之单例模式(singleton)

    一、单例模式概述

      保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。

      由于单例模式只生成一个实例,减少了系统性能开销。所以当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

      单例模式可以在系统设置全局的访问点,优化共享资源访问,例如可以设计一个单例类,复制所有数据表的映射处理。

      1. 懒汉式

        1)线程不安全

        ① 构造器私有化,避免外部直接创建对象

        ② 声明一个私有的静态属性

        ③ 对外创建一个公共的静态方法访问该属性

          如果属性没有该对象,创建该对象

        2)线程安全 

          使用同步方法

        3)双重检测(线程安全)double checking DCL

          使用同步块

          好处:第一次是为了不必要的同步,第二次是在属性等于null的情况下才创建实例
      2. 饿汉式

        ① 构造器私有化,避免外部直接创建对象

        ② 声明一个私有的静态属性,同时创建该对象

        ③ 对外创建一个公共的静态方法访问该属性

          好处:类加载时就已经加载该类对象,当需要该对象时,若没有则创建,若有直接返回

      3. 静态内部类

        ① 构造器私有化,避免外部直接创建对象

        ② 使用静态内部类声明一个私有的静态属性,同时创建该对象

        ③ 对外创建一个公共的静态方法访问该属性

          好处:不调用静态方法不加载内部类,延缓加载,提高效率

      4.枚举

        枚举式 (jdk1.4及以前建议使用)

    二、单例模式的五种写法

      1.饿汉式

        优点:线程安全,效率高

        缺点:无法延时加载

     1 public class Singleton {
     2     
     3     private static Singleton instance = new Singleton();
     4     
     5     //私有化构造器
     6     private Singleton() {}
     7 
     8     //提供全局访问点
     9     public static Singleton getInstance() {
    10         return instance;
    11     }
    12     
    13 }
     1 public class Singleton {
     2     
     3     private static Singleton instance = null;
     4     
     5     static {
     6         instance = new Singleton();
     7     }
     8     
     9     //私有化构造器
    10     private Singleton() {}
    11 
    12     //提供全局访问点
    13     public static Singleton getInstance() {
    14         return instance;
    15     }
    16     
    17 }

      2. 懒汉式

        优点:线程安全,延时加载

        缺点:效率较低

        (1)非线程安全

     1 public class Singleton {
     2     
     3     private static Singleton instance;
     4     
     5     //私有化构造器
     6     private Singleton() {}
     7 
     8     //提供一个全局的访问点
     9     public static Singleton getInstance() {
    10         if (instance == null) {
    11             instance = new Singleton();
    12         }
    13         return instance;
    14     }
    15     
    16 }

        (2)线程安全

     1 public class Singleton {
     2     
     3     private static Singleton instance;
     4     
     5     //私有化构造器
     6     private Singleton() {}
     7 
     8     //使用同步方法获取该类对象
     9     public static synchronized Singleton getInstance() {
    10         if (instance == null) {
    11             instance = new Singleton();
    12         }
    13         return instance;
    14     }
    15     
    16 }
     1 public class Singleton {
     2     
     3     private static Singleton instance;
     4     
     5     //私有化构造器
     6     private Singleton() {}
     7 
     8     //使用同步块获取该类对象
     9     public static Singleton getInstance() {
    10         synchronized (Singleton.class) {
    11             if (instance == null) {
    12                 instance = new Singleton();
    13             }
    14             return instance;
    15         }
    16     }
    17     
    18 }

      3.双重检查锁

        注意:由于编译器优化和JVM底层内部模型原因,偶尔会出问题,不建议使用

        优点:线程安全,延时加载

        缺点:效率较低,会出错误

     1 public class Singleton {
     2     
     3     private static Singleton instance;
     4     
     5     private Singleton() {}
     6     
     7     //第一次判断是为了避免不必要的同步,第二次判断是属性为null时创建实例
     8     public static Singleton getInstance() {
     9         if (instance == null) {
    10             synchronized (Singleton.class) {
    11                 if (instance == null) {
    12                     instance = new Singleton();
    13                 }
    14             }
    15         }
    16         return instance;
    17     }
    18 
    19 }

      4.静态内部类式

        不调用静态方法不加载内部类,延缓加载(懒加载),提高效率

        优点:线程安全,延时加载,效率高

     1 public class Singleton {
     2     
     3     //静态内部类
     4     private static class SingletonHolder {
     5         //final可加可不加,因为外部类的外部无法使用该内部类
     6         private static /*final*/ Singleton instance = new Singleton();
     7     }
     8     
     9     //私有化构造器
    10     private Singleton() {}
    11 
    12     //提供一个全局的访问点
    13     public static Singleton getInstance() {
    14         return SingletonHolder.instance;
    15     }
    16     
    17 }

      5.枚举

        注意:建议使用

        优点:实现简单,线程安全,效率高,由于JVM从根本上实现保障,避免反射和反序列化的漏洞

        缺点:无延时加载

    1 1 public enum Singleton {
    2 2     //这个枚举元素,本身就是一个单例对象
    3 3     INSTANCE;
    4 4 }

    三、测试五种单例模式的耗时问题

     1 /**
     2     在多线程环境下测试使用单例设计模式时创建对象的耗时(100个线程创建10000个对象)
     3        饿汉式:15ms
     4        懒汉式:671ms
     5        双重检测锁:65ms
     6        静态内部类:23ms
     7        枚举:32ms
     8  * @author CL
     9  *
    10  */
    11 public class TestRuntime {
    12     
    13     public static void main(String[] args) throws Exception {
    14         long start = System.currentTimeMillis();
    15         
    16         int threadNum = 100;
    17         final CountDownLatch countDownLatch = new CountDownLatch(threadNum);
    18         //匿名内部类
    19         for (int i = 0; i < threadNum; i++) {
    20             new Thread(new Runnable() {
    21                 
    22                 @Override
    23                 public void run() {
    24                     for(int i = 0; i < 10000; i++) {
    25                             //依次测试
    26                             Object o1 = SingletonDemo01.getInstance();    //饿汉式
    27 //                            Object o2 = SingletonDemo02.getInstance();    //懒汉式
    28 //                            Object o3 = SingletonDemo03.getInstance();    //双重检查锁
    29 //                            Object o4 = SingletonDemo04.getInstance();    //静态内部类
    30 //                            Object o5 = SingletonDemo05.INSTANCE;        //枚举
    31                     }
    32                 }
    33             }).start();
    34             countDownLatch.countDown();    //递减锁存器的计数,如果计数到达零,则释放所有等待的线程
    35         }
    36         
    37         countDownLatch.await();    //阻塞main线程,知道计数器为0才开始继续执行
    38         
    39         long end = System.currentTimeMillis();
    40         System.out.println("总耗时:"+(end - start)+"ms");
    41     }
    42 
    43 }

      注意,根据电脑配置等因素时间会有差异,但时间比大致相同。

      如何选用单例模式?

        (1)单例对象占用资源少,不需要延时加载时:枚举式好于饿汉式

        (2)单例对象占用资源大,需要延时加载时:静态内部类式好于懒汉式

    四、破解单例模式 

      1.使用反序列化破解单例模式

        步骤:(1)使用序列化将已经创建的对象写出到系统文件

              (2)使用反序列化读取文件中的对象

     1 import java.io.Serializable;
     2 
     3 /**
     4  * 使用反序列化破解单例模式
     5  * @author CL
     6  *
     7  */
     8 public class Singleton implements Serializable {
     9     
    10     private static class SingletonHolder {
    11         private static Singleton instance = new Singleton();
    12     }
    13     
    14     private Singleton() {}
    15 
    16     public static Singleton getInstance() {
    17         return SingletonHolder.instance;
    18     }
    19     
    20 }
     1 import java.io.FileInputStream;
     2 import java.io.FileOutputStream;
     3 import java.io.ObjectInputStream;
     4 import java.io.ObjectOutputStream;
     5 
     6 /**
     7  * 测试反序列化破解单例模式
     8  * @author CL
     9  *
    10  */
    11 public class TestSingleton {
    12     
    13     public static void main(String[] args) throws Exception {
    14         //先获得一个对象
    15         Singleton s01 = Singleton.getInstance();
    16         System.out.println("先获取的对象:" + s01);
    17         
    18         //(1)使用序列化将对象写出到系统文件
    19         String filePath = "C:\file\obj.txt";
    20         ObjectOutputStream oos = new ObjectOutputStream(
    21                 new FileOutputStream(filePath));
    22         oos.writeObject(s01);
    23         oos.flush();
    24         oos.close();
    25         
    26         //(2)使用反序列化读取文件中的对象
    27         ObjectInputStream ois = new ObjectInputStream(
    28                 new FileInputStream(filePath));
    29         Singleton s02 = (Singleton) ois.readObject();
    30         ois.close();
    31         
    32         System.out.println("反序列化读取的对象:" + s02);
    33     }
    34 
    35 }

      控制台输出:

    先获取的对象:com.caolei.singleton.Singleton@15db9742
    反序列化读取的对象:com.caolei.singleton.Singleton@33909752

      显然已经通过反序列化已经创建了新的对象,解决办法是在类中添加如下代码:

    1 /**
    2  * 反序列化时,如果创建了readResolve()方法则直接返回已经创建好的对象,而不需要再重新创建新的对象
    3  * @return
    4  * @throws ObjectStreamException
    5  */
    6  private Object readResolve() throws ObjectStreamException {
    7      return instance;
    8  }

      现在再测试:

     1 import java.io.ObjectStreamException;
     2 import java.io.Serializable;
     3 
     4 /**
     5  * 使用反序列化破解单例模式
     6  * @author CL
     7  *
     8  */
     9 public class Singleton implements Serializable {
    10     
    11     private static class SingletonHolder {
    12         private static Singleton instance = new Singleton();
    13     }
    14     
    15     private Singleton() {}
    16 
    17     public static Singleton getInstance() {
    18         return SingletonHolder.instance;
    19     }
    20     
    21     //反序列化时,如果创建了readResolve()方法则直接返回已经创建好的对象,而不需要再重新创建新的对象
    22     private Object readResolve() throws ObjectStreamException {
    23         return SingletonHolder.instance;
    24     }
    25     
    26 }
     1 import java.io.FileInputStream;
     2 import java.io.FileOutputStream;
     3 import java.io.ObjectInputStream;
     4 import java.io.ObjectOutputStream;
     5 
     6 /**
     7  * 测试反序列化破解单例模式
     8  * @author CL
     9  *
    10  */
    11 public class TestSingleton {
    12     
    13     public static void main(String[] args) throws Exception {
    14         //先获得一个对象
    15         Singleton s01 = Singleton.getInstance();
    16         System.out.println("先获取的对象:" + s01);
    17         
    18         //(1)使用序列化将对象写出到系统文件
    19         String filePath = "C:\file\obj.txt";
    20         ObjectOutputStream oos = new ObjectOutputStream(
    21                 new FileOutputStream(filePath));
    22         oos.writeObject(s01);
    23         oos.flush();
    24         oos.close();
    25         
    26         //(2)使用反序列化读取文件中的对象
    27         ObjectInputStream ois = new ObjectInputStream(
    28                 new FileInputStream(filePath));
    29         Singleton s02 = (Singleton) ois.readObject();
    30         ois.close();
    31         
    32         System.out.println("反序列化读取的对象:" + s02);
    33     }
    34 
    35 }

      控制台输出:

    先获取的对象:com.caolei.singleton.Singleton@15db9742
    反序列化读取的对象:com.caolei.singleton.Singleton@15db9742

      现在创建的对象是同一对象,避免了反序列化破解单例模式出现的问题。

      2.使用反射破解单例模式

        步骤:(1)获取对象

              (2)获取构造器

              (3)跳过安全检查

              (4)创建对象

     1 /**
     2  * 使用反射破解单例模式
     3  * @author CL
     4  *
     5  */
     6 public class Singleton {
     7     
     8     private static class SingletonHolder {
     9         private static Singleton instance = new Singleton();
    10     }
    11     
    12     private Singleton() {}
    13 
    14     public static Singleton getInstance() {
    15         return SingletonHolder.instance;
    16     }
    17     
    18 }
     1 import java.lang.reflect.Constructor;
     2 
     3 /**
     4  * 测试反射破解单例模式
     5  * @author CL
     6  *
     7  */
     8 public class TestSingleton {
     9 
    10     public static void main(String[] args) throws Exception {
    11         Singleton s1 = Singleton.getInstance();
    12         System.out.println("先获取的对象:" + s1);
    13 
    14         //使用反射破解单例模式
    15         //(1)创建对象
    16         Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.caolei.singleton.Singleton");
    17         //(2)获得构造器
    18         Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
    19         //(3)跳过安全检查
    20         c.setAccessible(true);
    21         //(4)创建对象
    22         Singleton s2 = c.newInstance();
    23         
    24         System.out.println("反射获取的对象:" + s2);
    25     }
    26 }

      控制台输出:

    先获取的对象:com.caolei.singleton.Singleton@15db9742
    反射获取的对象:com.caolei.singleton.Singleton@6d06d69c

      显然使用反射机制跳过安全检查通过私有的构造器创建了新的对象,解决办法是在私有的构造器中添加如下代码:

    1 if (instance != null) {
    2     throw new RuntimeException();    //再次创建对象时抛出异常
    3 }

      现在再测试:

     1 /**
     2  * 使用反射破解单例模式
     3  * @author CL
     4  *
     5  */
     6 public class Singleton {
     7     
     8     private static class SingletonHolder {
     9         private static Singleton instance = new Singleton();
    10     }
    11     
    12     private Singleton() {
    13         if (SingletonHolder.instance != null) {
    14             throw new RuntimeException();    //再次创建对象时抛出异常
    15         }
    16     }
    17 
    18     public static Singleton getInstance() {
    19         return SingletonHolder.instance;
    20     }
    21     
    22 }
     1 import java.lang.reflect.Constructor;
     2 
     3 /**
     4  * 测试反射破解单例模式
     5  * @author CL
     6  *
     7  */
     8 public class TestSingleton {
     9 
    10     public static void main(String[] args) throws Exception {
    11         Singleton s1 = Singleton.getInstance();
    12         System.out.println("先获取的对象:" + s1);
    13 
    14         //使用反射破解单例模式
    15         //(1)创建对象
    16         Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.caolei.singleton.Singleton");
    17         //(2)获得构造器
    18         Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
    19         //(3)跳过安全检查
    20         c.setAccessible(true);
    21         //(4)创建对象
    22         Singleton s2 = c.newInstance();
    23         
    24         System.out.println("反射获取的对象:" + s2);
    25     }
    26 }

      控制台输出:

    先获取的对象:com.caolei.singleton.Singleton@15db9742
    Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:408)
        at com.caolei.singleton.TestSingleton.main(TestSingleton.java:24)
    Caused by: java.lang.RuntimeException
        at com.caolei.singleton.Singleton.<init>(Singleton.java:16)
        ... 5 more

     五、单例模式常见的应用场景

      (1)Windows的Task Manager(任务管理器)、Recycle Bin(回收站)、文件系统等都是典型的单例模式;

      (2)在项目中,读取配置文件的类一般也只有一个对象,没有必要每次使用配件的数据时,都去new一个对象来获取;

      (3)网站的计数器,一般也是采用单例模式实现,否则难以同步;

      (4)数据库连接池的设计就是采用单例模式;

      (5)在Spring中,每个Bean默认是单例的,便于Spring容器管理;

      (6)每个Servlet都是单例的;

      (7)………………

  • 相关阅读:
    Notification 通知
    首次在MI5手机上看到APP界面 ~
    Installation falied with message Failed to establish session.
    adb.exe 已停止工作
    内容提供器(Content Provider)
    Android 数据存储
    RecyclerView
    UI设计 四种基本布局
    关于Android教学的思考1
    Android 主要控件
  • 原文地址:https://www.cnblogs.com/cao-lei/p/8085529.html
Copyright © 2011-2022 走看看