• 设计模式 -- 单例模式(Singleton)


    写在前面的话:读书破万卷,编码如有神
    --------------------------------------------------------------------
    主要内容包括:

    1. 初始单例模式,包括:定义、结构、参考实现
    2. 体会单例模式,场景问题、不用模式的解决方案、使用模式的解决方案
    3. 理解单例模式,包括:认识单例模式、懒汉式和饿汉式实现、延迟加载的思想、缓存的思想、Java中缓存的基本实现、利用缓存来实现单例模式、单例模式的优缺点、在Java中一种更好的单例实现方式、单例和枚举
    4. 思考单例模式,包括:单例模式的本质、何时选用

    参考内容:

    1、《研磨设计模式》 一书,作者:陈臣、王斌

    ---------------------------------------------------------------------

    1、初识单例模式                                                                    

    1.1、定义

      保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    1.2、结构和说明

    Singleton: 负责创建Singleton类自己的唯一实例,并提供一个getInstance的方法,让外部来访问这个类的唯一实例。

    1.3、示例代码

    在Java中,单例模式的实现又分为两种:懒汉式、饿汉式,下面分别来看看这两种的实现代码:

    (1)懒汉式示例代码如下:

     1 /**
     2  * 懒汉式单例实现的示例代码
     3  */
     4 public class Singleton {
     5     //定义一个变量来存储定义好的类实例
     6     private static Singleton instance = null;
     7     
     8     /**
     9      * 私有化构造方法,可以在内部控制创建实例的个数
    10      */
    11     private Singleton(){
    12         
    13     }
    14     
    15     /**
    16      * 定义一个方法来为客户端提供类实例
    17      * @return 一个Singleton的实例
    18      */
    19     public static synchronized Singleton getInstance(){
    20         //判断存储实例的变量是否有值
    21         if(instance == null){
    22             //如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
    23             instance = new Singleton();
    24         }
    25         return instance;
    26     }
    27     
    28     /**
    29      * 单例模式也可以有自己的操作
    30      */
    31     public void Operation(){
    32         
    33     }
    34     
    35     //单例模式也可以有自己的属性
    36     private String data;
    37 
    38     public String getData() {
    39         return data;
    40     }
    41 
    42     public void setData(String data) {
    43         this.data = data;
    44     }
    45 }
    View Code

    (2)饿汉式示例代码如下:

     1 /**
     2  * 饿汉式单例实现的示例代码
     3  */
     4 public class Singleton {
     5     //定义一个变量来存储定义好的类实例
     6     private static Singleton instance = new Singleton();
     7     
     8     /**
     9      * 私有化构造方法,可以在内部控制创建实例的个数
    10      */
    11     private Singleton(){
    12         
    13     }
    14     
    15     /**
    16      * 定义一个方法来为客户端提供类实例
    17      * @return 一个Singleton的实例
    18      */
    19     public static synchronized Singleton getInstance(){
    20         return instance;
    21     }
    22     
    23     /**
    24      * 单例模式也可以有自己的操作
    25      */
    26     public void Operation(){
    27         
    28     }
    29     
    30     //单例模式也可以有自己的属性
    31     private String data;
    32 
    33     public String getData() {
    34         return data;
    35     }
    36 
    37     public void setData(String data) {
    38         this.data = data;
    39     }
    40 }
    View Code

    ---------------------------------------------------------------------

    2、体会单例模式                                                                    

    2.1、场景问题

      考虑这样一个应用,读取配置文件的内容,在实际的项目中配置文件多采用xml格式或者properties格式的,现在要读取配置文件的内容,该如何实现呢?

    2.2、不用模式的解决方案

    假设系统采用的是properties格式的配置文件,读取配置文件的示例代码如下:

     1 (1)jdbc.properties文件的内容
     2     jdbcUser=xixixixixi
     3     jdbcPassword=123456
     4 
     5 (2)读取配置文件应用程序
     6 import java.io.IOException;
     7 import java.io.InputStream;
     8 import java.util.Properties;
     9 
    10 /**
    11  * 读取应用配置文件
    12  */
    13 public class AppConfig {
    14     //数据库用户名
    15     private String jdbcUser;
    16     //数据库密码
    17     private String jdbcPassword;
    18     
    19     /**
    20      * 构造函数
    21      */
    22     public AppConfig(){
    23         //调用读取配置文件的方法
    24         readConfig();
    25     }
    26     
    27     /**
    28      * 读取配置文件,把配置文件中的内容读取出来设置到属性上
    29      */
    30     private void readConfig(){
    31         Properties p = new Properties();
    32         InputStream in = null;
    33         
    34         try{
    35             in = AppConfig.class.getResourceAsStream("jdbc.properties");
    36             p.load(in);
    37             
    38             //把配置文件中的内容读取出来设置到属性上
    39             jdbcUser = p.getProperty("jdbcUser");
    40             jdbcPassword = p.getProperty("jdbcPassword");
    41         }catch(IOException e){
    42             e.printStackTrace();
    43         }finally{
    44             try {
    45                 in.close();
    46             } catch (IOException e) {
    47                 e.printStackTrace();
    48             }
    49         }
    50     }
    51 
    52     public String getJdbcUser() {
    53         return jdbcUser;
    54     }
    55 
    56     public String getJdbcPassword() {
    57         return jdbcPassword;
    58     }
    59 }
    60 
    61 (3)测试客户端
    62 public class Client {
    63     public static void main(String[] args) {
    64         AppConfig appConfig = new AppConfig();
    65         
    66         String jdbcUser = appConfig.getJdbcUser();
    67         String jdbcPassword = appConfig.getJdbcPassword();
    68         
    69         System.out.println("jdbcUser = " + jdbcUser);
    70         System.out.println("jdbcPassword = " + jdbcPassword);
    71     }
    72 }
    73 
    74 (4)运行结果
    75 jdbcUser = xixixixixi
    76 jdbcPassword = 123456
    View Code

    2.3、有何问题

    上面的实现很简单,很容易就实现要求的功能了。但是来仔细想想,有没有什么问题?

    存在的问题:在客户端中是通过new一个AppConfig的实例来得到一个操作配置文件内容的对象,如果在系统运行中,有很多地方都需要使用配置文件的内容,也就是说很多地方都需要创建AppConfig对象的实例。----->换句话说,在系统运行期间,系统中会存在很多个AppConfig的实例对象,每一个AppConfig实例对象里面都封装这配置文件的内容,系统中有多个AppConfig实例对象,也就是说系统中会同时存在多份配置文件的内容,这样会严重浪费内存资源

    把问题抽象一下:在一个系统运行期间,某个类只需要一个类实例就可以了,那么该怎么实现呢?

    2.4、使用单例模式来解决问题

    (1)解决问题的思路

      现在AppConfig类能被创建多个实例,问题的根源在于类的构造方法是公开的,也就是可以让类的外部来通过构造方法创建多个实例。换句话说,只要类的构造方法能让类的外部访问,就没有办法去控制外部来创建这个类的实例个数。

      要想控制一个类只被创建一个实例,那么首要的问题就是要把创建实例的权限收回来,让类自身来负责自己类实例的创建工作,然后由这个类来提供外部可以访问这个类实例的方法,这就是单例模式的实现方式。

    (2)使用单例模式重写示例(饿汉式)

     1 (1)properties属性文件
     2 jdbcUser=xixixixixi
     3 jdbcPassword=123123
     4 
     5 (2)采用单例模式实现读取配置文件程序
     6 import java.io.IOException;
     7 import java.io.InputStream;
     8 import java.util.Properties;
     9 
    10 /**
    11  * 读取应用配置文件,单例模式实现
    12  */
    13 public class AppConfig {
    14     //数据库用户名
    15     private String jdbcUser;
    16     //数据库密码
    17     private String jdbcPassword;
    18     
    19     //定义一个变量来存储创建好的类实例
    20     private static AppConfig instance = new AppConfig();
    21     
    22     /**
    23      * 私有化构造函数
    24      */
    25     private AppConfig(){
    26         //调用读取配置文件的方法
    27         readConfig();
    28     }
    29     
    30     /**
    31      * 定义一个方法来为客户端提供AppConfig类的实例
    32      * @return AppConfig类的实例
    33      */
    34     public static AppConfig getInstance(){
    35         return instance;
    36     }
    37     
    38     /**
    39      * 读取配置文件,把配置文件中的内容读取出来设置到属性上
    40      */
    41     private void readConfig(){
    42         Properties p = new Properties();
    43         InputStream in = null;
    44         
    45         try{
    46             in = AppConfig.class.getResourceAsStream("jdbc.properties");
    47             p.load(in);
    48             
    49             //把配置文件中的内容读取出来设置到属性上
    50             jdbcUser = p.getProperty("jdbcUser");
    51             jdbcPassword = p.getProperty("jdbcPassword");
    52         }catch(IOException e){
    53             e.printStackTrace();
    54         }finally{
    55             try {
    56                 in.close();
    57             } catch (IOException e) {
    58                 e.printStackTrace();
    59             }
    60         }
    61     }
    62 
    63     public String getJdbcUser() {
    64         return jdbcUser;
    65     }
    66 
    67     public String getJdbcPassword() {
    68         return jdbcPassword;
    69     }
    70 }
    71 
    72 (3)客户端
    73 public class Client {
    74     public static void main(String[] args) {
    75         
    76         //获取读取应用配置文件的对象
    77         AppConfig appConfig = AppConfig.getInstance();
    78     
    79         String jdbcUser = appConfig.getJdbcUser();
    80         String jdbcPassword = appConfig.getJdbcPassword();
    81         
    82         System.out.println("jdbcUser = " + jdbcUser);
    83         System.out.println("jdbcPassword = " + jdbcPassword);
    84     }
    85 }
    86 
    87 (4)运行结果
    88 jdbcUser = xixixixixi
    89 jdbcPassword = 123123
    View Code

    ---------------------------------------------------------------------

    3、理解单例模式                                                                    

    3.1、认识单例模式

    (1)单例模式的功能

      单例模式是用来保证这个类在运行期间只会被创建一个类实例,并且还提供了一个全局唯一访问这个类实例的访问点。

    (2)单例模式的范围

      目前Java里面实例的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的。

    3.2、延迟加载的思想

      单例模式的懒汉式实现方式体现了延迟加载的思想,什么是延迟加载呢? 通俗点说,延迟加载就是一开始不要加载资源或者数据,等到要使用的时候再去加载,所以也称为 Lazy Load,这在实际开发中是一种很常见的思想,尽可能地节约资源。

    3.3、缓存的思想

      单例模式的懒汉式实现还体现了缓存的思想,简单讲就是,某些资源或者数据被频繁地使用,而这些资源或数据存在在系统外部,比如数据库、磁盘文件等,那么每次操作这些数据的时候都得从数据库或者硬盘上去获取,速度会很慢,将造成性能问题。

      解决办法就是利用缓存: 把这些数据缓存到内存里面,每次操作的时候,先到内存里面找,看有没有这些数据,如果有,就直接使用;如果没有,就去获取它并设置到缓存中,下一次访问的时候就可以直接从内存中获取了,从而节省大量的时间

    ps:缓存是一种典型的空间换时间的方案

    3.4、Java中缓存的基本实现

    在实际开发中常用的缓存方法有很多,比如:memcached、ehcache等。

    在Java开发中最常见的一种实现缓存的方式就是使用Map,基本步骤如下:

    1. 先到缓存里面查找,看看是否存在需要使用的数据
    2. 如果没有找到,那么就创建一个满足要求的数据,然后把这个数据设置到缓存中,以备下次使用。
    3. 如果找到了相应的数据,或者创建了相应的数据,那么久直接使用这个数据。

    示例代码如下:

     1 import java.util.HashMap;
     2 import java.util.Map;
     3 
     4 /**
     5  * Java中缓存的基本实现示例
     6  */
     7 public class JavaCache {
     8     /**
     9      * 缓存数据的容器
    10      */
    11     private Map<String,Object> map = new HashMap<String,Object>();
    12     
    13     /**
    14      * 从缓存中获取值
    15      * @param key 设置时候的key值
    16      * @return key对应的value值
    17      */
    18     public Object getValue(String key){
    19         //先从缓存中去获取
    20         Object obj = map.get(key);
    21         
    22         //判断缓存里面是否有值
    23         if(obj == null){
    24             //如果没有,那么就去获取相应的数据,比如读取数据库或者文件
    25             //这里只是演示,所以直接写个假值
    26             obj = key + ",value";
    27             
    28             //把获取的值设置回到缓存里面
    29             map.put(key, obj);
    30         }
    31         
    32         //如果有值了,就直接使用
    33         return obj;
    34     }
    35 }
    View Code

    3.5、单例模式的优缺点

    (1)时间和空间

    • 懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
    • 饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。

    (2)线程安全

    • 不加同步的懒汉式是线程不安全的,如果有两个线程同时调用getInstance方法,那么可能会导致并发问题。
    • 饿汉式是线程安全的,因此虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。

    (3)如何实现懒汉式的线程安全呢?

    只要加上synchronized即可,如下:

    1 public static synchronized Singleton getInstance()

    但是这在方法上加同步控制,会降低整个访问速度,效率比较低。

    (4)双重检查加锁

    所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查;进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

    示例代码如下:

     1 import java.io.IOException;
     2 import java.io.InputStream;
     3 import java.util.Properties;
     4 
     5 /**
     6  * 读取应用配置文件,单例模式实现(双重检查方式)
     7  */
     8 public class AppConfig {
     9     //数据库用户名
    10     private String jdbcUser;
    11     //数据库密码
    12     private String jdbcPassword;
    13     
    14     //定义一个变量来存储创建好的类实例
    15     private volatile static AppConfig instance = null;
    16     
    17     /**
    18      * 私有化构造函数
    19      */
    20     private AppConfig(){
    21         //调用读取配置文件的方法
    22         readConfig();
    23     }
    24     
    25     /**
    26      * 定义一个方法来为客户端提供AppConfig类的实例
    27      * @return AppConfig类的实例
    28      */
    29     public static AppConfig getInstance(){
    30         //先检查实例是否存在,如果不存在则进入下面的同步块
    31         if(instance == null){
    32             //同步块,线程安全的创建实例
    33             synchronized(AppConfig.class){
    34                 //再次检查实例是否存在,如果不存在才真正的创建实例
    35                 if(instance == null){
    36                     instance = new AppConfig();
    37                 }
    38             }
    39         }
    40         
    41         return instance;
    42     }
    43     
    44     /**
    45      * 读取配置文件,把配置文件中的内容读取出来设置到属性上
    46      */
    47     private void readConfig(){
    48         Properties p = new Properties();
    49         InputStream in = null;
    50         
    51         try{
    52             in = AppConfig.class.getResourceAsStream("jdbc.properties");
    53             p.load(in);
    54             
    55             //把配置文件中的内容读取出来设置到属性上
    56             jdbcUser = p.getProperty("jdbcUser");
    57             jdbcPassword = p.getProperty("jdbcPassword");
    58         }catch(IOException e){
    59             e.printStackTrace();
    60         }finally{
    61             try {
    62                 in.close();
    63             } catch (IOException e) {
    64                 e.printStackTrace();
    65             }
    66         }
    67     }
    68 
    69     public String getJdbcUser() {
    70         return jdbcUser;
    71     }
    72 
    73     public String getJdbcPassword() {
    74         return jdbcPassword;
    75     }
    76 }
    View Code

    双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存的,从而确保多个线程能正确的处理该变量

    3.6、在Java中一种更好的单例实现方式

    下面这种方式既能够实现延迟加载,又能够实现线程安全(采用类级内部类的方式)。

    示例代码:

     1 public class Singleton {
     2     
     3     /**
     4      * 私有化构造方法
     5      */
     6     private Singleton(){
     7         
     8     }
     9 
    10     /**
    11      * 类级的内部类,也就是静态的成员内部类,该内部类的实例与外部类的实例没有绑定关系,
    12      * 而且只有被调用到时才会装载,从而实现延迟加载
    13      */
    14     private static class SingletonHolder{
    15         /**
    16          * 静态初始化器,由JVM来保证线程安全
    17          */
    18         private static Singleton instance = new Singleton();
    19     }
    20     
    21     public static Singleton getInstance(){
    22         return SingletonHolder.instance;
    23     }
    24 }

    上面的代码使用了Java的类级内部类多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。

    相关的基础知识:

    (1)类级内部类

    • 什么是类级内部类? 简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
    • 类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
    • 类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
    • 类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。

    (2)多线程缺省同步锁的知识

    在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况下,JVM已经隐含地为我们执行了同步,这些情况下就不用自己再来进行同步控制了,这些情况包括:

    • 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
    • 访问final字段时
    • 在创建线程之前创建对象时
    • 线程可以看见它将要处理的对象时

    3.7、单例和枚举

    3.8、思考单例模式

    (1)单例模式的本质: 控制实例数目。

    (2)何时选用单例模式

      当需要控制一个类的实例只能有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式。

    ---------------------------------------------------------------------

    ---------------------------------------------------------------------

    ---------------------------------------------------------------------  

  • 相关阅读:
    Java Jsch SFTP 递归下载文件夹
    spring-jms,spring-boot-starter-activemq JmsTemplate 发送方式
    Spring Boot 入门之消息中间件篇(转发)
    Springboot websocket使用
    FinalCutPro快捷键
    基本CSS布局三
    As Simple as One and Two
    Game of Credit Cards
    WOW Factor
    Lose it!
  • 原文地址:https://www.cnblogs.com/xinhuaxuan/p/6424926.html
Copyright © 2020-2023  润新知