创建型模式-单例模式

学习目标:

  • 什么是单例模式?
  • 为什么会有单例模式?即他的用处,以及它解决了什么问题
  • 怎样事项单例模式,即它的设计思想
  • 单例模式有哪些写法?
  • 单例模式在面试中有哪些注意事项?

1、为什么会有单例模式?

  1. 我们都知道,单例模模式是在开发中最常用的一种设计模式,
  2. 例模式主要是为了避免因为创建多个实例造成的资源浪费,且多个实力因为多次调用容易导致结果出现错误,而使用单例模式可以保证整个应用中有且只有一个实例。
  3. 可以解决的问题:可以保证一个类在内存中对象的唯一性,在一些常用的工具类、线程池、缓存、数据库、账户登录、配置文件等程序中可能只允许我们创建一个对象;一方面,如果创建多个对象可能引起程序错误,另一方面,创建多个对象也造成了系统资源的浪费,在这种需求下,单例模式就诞生了。

加入有这么一个需求,有一个类A 和 类B,以及他们的共享配置文件信息,在这个配置文件中有很多数据如下图所示:

如上图所示:在类ConfigFile中,存在共享数据 Num1, Num2, Num3... ... 加入在类A 中修改ConfigFile中的数据,在类A 中应该有如下代码,

ConfigFile configFile = new ConfigFile();
configFile.num1 = 2;

此时,configFile 中的Num1 = 2, 但是请注意,这里是new ConfigFile(),是一个对象,在进行了上述操作后,类B 中进行如下操作

ConfigFile configFile = new ConfigFile();
System.out.println( "configFile.Num1 = " + congilFile.Num1 );

即直接new ConfigFile();然后打印,Num1,大家思考一下,打印的是即?

我们应该知道,打印结果是这样的: configFile.Num1 = 1;

也就是说每次调用都创建了一个ConfigFile对象,所以导致了在类A 中修改并不会真正改变ConfigFile中的值,它所更改的只是在类A 中创建的那个对象的值。

加入现在在类A 中修改数据后要通知B , 即在类A 和类B 中操作的是同一个数据,类A 中改变数据,类B 中也会得到该数据,并在类A 修改后的基础上进行操作,到这里可能会说将ConfigFile 类中的所有数据改成静态的就可以了,虽然可以,但是这样对内存是一个极大的消耗,。所以将变量设置成静态的虽然可行,但并不是一个很好的解决方案。这里,就需要用到单例模式了!!

2、单例模式的思想

在上面的问题中,我们说到,要解决这样的问题的关键就是保证应用中只用一个对象就行了,那么,怎么保证只用一个呢?

其实,只要三步,就可以保证一个对象的唯一性:

  1. 不允许其他程序new 对象
    • 因为new 就是开辟新的空间,在这里更改数据只是更改所创建的对象的数据,如果可以new 的话,每次new 都产生一个新的对象,这样肯定保证不了对象的唯一性。
  2. 在该类中创建对象,
    • 因为不允许其他程序new 对象,所以这里的对象需要在本类中new出来,
  3. 对外提供一个可以让其他程序获取该对象的方法
    • 因为对象是在本类中创建,所以需要提供一个方法,让其他类获取这个对象。

那么,上述这省三步在代码中是怎样实现的呢?将上述三步转换成代码描述是这样的,

1>  私有化该类的构造函数;

2> 通过new 在本类中创建一个本类对象;

3> 定义一个共有的方法,将在该类中创建的对象返回;

3、单例模式的写法

经过2 中的分析,我们了解了单例模式所解决的问题以及他的思想,接下来看看他的代码实现方式,单例模式的写法大致可分为5种

  • 懒汉式
  • 饿汉式
  • 双重检验锁
  • 静态内部类
  • 枚举

接下来就来看看这几种模式的代码实现,以及他们的优缺点:

  • 饿汉式

    public class Singleton{
        public static Singleton instance = new Singleton();
        
        private Singleton (){}
        
        public static Singleton getInstance (){
            return instance;
        }
    }
    

    访问方式:

    Singleton instance = Singleton.getInstance();
    

    得到这个对象之后,既可以访问类中的方法了,

    优点:从他的实现种我们可以看到,这种实现的方式比较简单,在类加载的时候就完成了实例化,避免了线程的同步;

    缺点:由于在加载的时候就实例化了,所以没有达到 Lazy Loading (懒加载) 的效果,也就是说,我可能没用到这个对象,但它也会加载,会造成内存的浪费(但是可以忽略不记,这种方式也是推荐使用的)

  • 饿汉式

    public class Singleton{
        public static Singleton instance = null;
        
        static {
            instance = new Singleton();
        }
        
        private Singleton(){}
        
        public static Singleton getInstance(){
            return instance;
        }   
    }
    

    访问方式,

    Singleton instance = Singleton.getInstance();
    

    得到这个实例后,就可以访问类中的方法了,

    可以看到,上面的代码,在类创建的时候就已经实例化了对象。

  • 懒汉式【线程不安全,不可用】

    public class SingletonA {
      private static SingletonA instance = null;
    
      private SingletonA(){};
    
      /**
       * 方式一:
       * 有线程安全问题
       * @return
       */
      public static SingletonA getInstance() {
          if ( instance == null ){
              instance = new SingletonA();
          }
          return instance;
      }
    }
    

这种方式式在调用getInstance 方法的时候将对象实例化的,因为比较懒,所以被称为懒汉式;

在上述写法中,懒汉式确实存在线程安全的问题,那么到底存在怎样线程安全的问题?怎样导致这种问题的?接下来说说:

在运行过程中存在这么一种情况,有多个线程去电泳getInstance 方法来获得Singleton 实例,那么就可能发生这样一种情况:当线程A 去执行 =if ( instance == null )= 时,此时 instance 为 null 进入语句,因为这个时候,线程还没有执行 instance = new Singleton(), 此时线程B 也进入了if语句,所以线程A、线程B 都会 执行instance = new Singleton() 来实例化对象。这样就导致实例化了两个Singleton 对象,所以单例模式的懒汉式是存在线程安全问题的,既然它存在问题,那么就应该有解决问题的方法,怎么解决呢,有人可能想到加锁去解决这个问题,于是有了下面的写法。

  • 懒汉式线程安全【线程安全,效率低下不推荐使用】

        public class SingletonA {
            private static SingletonA instance = null;
    
            private SingletonA(){};
      
            /**
             * 方式二:
             * 解决懒汉线程安全【不推荐使用】
             */
            public static synchronized SingletonA getInstance(){
                if( null == instance ){
                    instance = new SingletonA();
                }
                return instance;
            }
    
            public static void main(String[] args) {
                SingletonA instance = SingletonA.getInstance();
                System.out.println(instance);
            }
        }
    
  • 懒汉式【双重检验锁,推荐使用】

    public class Singleton{
        /**
         * 懒汉式变种,属于懒汉式中最好的写法,保证了:延迟加载和线程安全
         **/
        private static Singleton instance = null;
        
        private Singleton(){};
        
        private static Singleton getInstance(){
          if( instance == null ){
              synchronized ( Singleton.class ){
                  if( instance == null ){
                      instance = new Singleton();
                  }
              }
          }
          return instance;
      }
    }
    

    访问方式:

    Singleton instance = Singleton.getInstance();
    

    Double-Check 对于线程开发者来说并不会陌生,如代码中所示,我们进行了两次if( instance == null ) 检查,这样就可以保证线程安全;

    优点:线程安全;延迟加载;效率较高;

  • 内部类【推荐使用】

     /**
       * 内部类方式
       */
      public class SingletonB {
    
          private SingletonB(){};
    
          private static class SingletonHolder{
              private static SingletonB instance = new SingletonB();
          }
    
          public static SingletonB getInstance(){
              return SingletonHolder.instance;
          }
    
          public static void main(String[] args) {
              SingletonB instance = SingletonB.getInstance();
              System.out.println(instance);
          }
      }
    

    访问方式:

    SingletonB instance = SingletonB.getInstance();
    

    这种方式和饿汉式采用的方法类似,但又有些不同,两者都是采用类装载机制,保证初始化实例时只有一个线程,不同的时,饿汉式只要Singleton 类被装在就会实例化,没有lazy-loading的作用,而静态内部类方式在Singleton 类被装载时并不会被实例化,而是在需要实例化时,调用getInstance 方式,才会装载SingletonHolder 类,从而完成Singleton 的实例化,类的静态属性只会在第一次加载类的时候被实例化,在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时别的线程时无法进入的。

    优点:避免了线程不安全;延迟加载;效率高。

  • 枚举【极推荐使用】

      public enum  SingletonEnum {
          instance,
          ;
          private SingletonEnum(){}
    
          public SingletonEnum method(){
              return instance;
          }
    
          public static void main(String[] args) {
              SingletonEnum.instance.method();
          }
      }
    

    访问方式

    SingletonEnum.instance.method();
    

    可以看到,枚举的使用方式非常简单;

    借助JDK1.5中的枚举来实现单例模式。不仅能避免线程同步问题,而且还能防止反序列化来重新创建新的对象。这种方式时最好的一种方式,在实际开发中,如果JDK满足要求的情况下,建议使用这种方式。

4.单例模式在面试种的问题

​ 单例模式在面试种常常会遇到,其中很少有人会问到饿汉式写法,一般都会问单例模式的懒汉式的线程安全问题,所以一定把大比例模式的线程安全问题理解清楚,认真学透

posted @ 2020-06-25 17:05  雷姆饲养员  阅读(97)  评论(0)    收藏  举报