zoukankan      html  css  js  c++  java
  • 单例模式(含多线程处理)

    版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习!

          单例,顾名思义一个类只有一个实例。为什么要使用单例模式,或者说什么样的类可以做成单例的?在工作中我发现,使用单例模式的类都有一个共同点,那就是这个类没有状态,也就是说无论你实例化多少个对象,其实都是一样的。又或者是一个类需要频繁实例化然后销毁对象。还有很重要的一点,如果这个类有多个实例的话,会产生程序错误或者不符合业务逻辑。这种情况下,如果我们不把类做成单例,程序中就会存在多个一模一样的实例,这样会造成内存资源的浪费,而且容易产生程序错误。总结一下,判断一个类是否要做成单例,最简单的一点就是,如果这个类有多个实例会产生错误,或者在整个应用程序中,共享一份资源。

         在实际开发中,一些资源管理器、数据库连接等常常设计成单例模式,避免实例重复创建。实现单例有几种常用的方式,下面我们来探讨一下他们各自的优劣。

         第一种方式:懒汉式单例

         

     1 public class Singleton {
     2     //一个静态实例
     3     private static Singleton singleton;
     4     //私有构造方法
     5     private Singleton(){
     6         
     7     }
     8     //提供一个公共静态方法来获取一个实例
     9     public static Singleton getInstance(){
    10         
    11         if(singleton == null ){
    12             
    13             singleton = new Singleton();
    14         }
    15         
    16         return singleton;
    17         
    18     }
    19 }

        在不考虑并发的情况下,这是标准的单例构造方式,它通过以下几个要点来保证我们获得的实例是单一的。

        1、静态实例,静态的属性在内存中是唯一的;

        2、私有的构造方法,这就保证了不能人为的去调用构造方法来生成一个实例;

        3、提供公共的静态方法来返回一个实例, 把这个方法设置为静态的是有原因的,因为这样我们可以通过类名来直接调用此方法(此时我们还没有获得实例,无法通过实例来调用方法),而非静态的方法必须通过实例来调用,因此这里我们要把它声明为静态的方法通过类名来调用;

        4、判断只有持有的静态实例为null时才通过构造方法产生一个实例,否则直接返回。

        在多线程环境下,这种方式是不安全,通过自己的测试,多个线程同时访问它可能生成不止一个实例,我们通过程序来验证这个问题:

        

     1 public class Singleton {
     2     //一个静态实例
     3     private static Singleton singleton;
     4     //私有构造方法
     5     private Singleton(){
     6         
     7     }
     8     //提供一个公共静态方法来获取一个实例
     9     public static Singleton getInstance(){
    10         
    11         if(singleton == null ){
    12             
    13             try {
    14                 Thread.sleep(5000);  //模拟线程在这里发生阻塞
    15             } catch (InterruptedException e) {
    16                 e.printStackTrace();
    17             }
    18             
    19             singleton = new Singleton();
    20         }
    21         
    22         return singleton;
    23         
    24     }
    25 }

        测试类:

    public class TestSingleton {
        
        public static void main(String[] args) {
            
            Thread t1 = new MyThread();
            Thread t2 = new MyThread();
            
            t1.start();
            t2.start();
        }
    
    }
    
    
    class MyThread extends Thread{
        
        @Override
        public void run() {
            
            System.out.println(Singleton.getInstance()); //打印生成的实例,会输出实例的类名+哈希码值
            
        }
    }

        执行该测试类,输出的结果如下:

        

        从以上结果可以看出,输出两个实例并且实例的hashcode值不相同,证明了我们获得了两个不一样的实例。这是什么原因呢?我们生成了两个线程同时访问getInstance()方法,在程序中我让线程睡眠了5秒,是为了模拟线程在此处发生阻塞,当第一个线程t1进入getInstance()方法,判断完singleton为null,接着进入if语句准备创建实例,同时在t1创建实例之前,另一个线程t2也进入getInstance()方法,此时判断singleton也为null,因此线程t2也会进入if语句准备创建实例,这样问题就来了,有两个线程都进入了if语句创建实例,这样就产生了两个实例。

        为了避免这个问题,在多线程情况下我们要考虑线程同步问题了,最简单的方式当然是下面这种方式,直接让整个方法同步:

        

    public class Singleton {
        //一个静态实例
        private static Singleton singleton;
        //私有构造方法
        private Singleton(){
            
        }
        //提供一个公共静态方法来获取一个实例
        public static synchronized Singleton getInstance(){
            
            if(singleton == null ){
                
                try {
                    Thread.sleep(5000);  //模拟线程在这里发生阻塞
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                singleton = new Singleton();
            }
            
            return singleton;
            
        }
    }

        我们通过给getInstance()方法加synchronized关键字来让整个方法同步,我们同样可以执行上面给出的测试类来进行测试,打印结果如下:

        

        从测试结果可以看出,两次调用getInstance()方法返回的是同一个实例,这就达到了我们单例的目的。这种方式虽然解决了多线程同步问题,但是并不推荐采用这种设计,因为没有必要对整个方法进行同步,这样会大大增加线程等待的时间,降低程序的性能。我们需要对这种设计进行优化,这就是我们下面要讨论的第二种实现方式。

        第二种方式:双重校验锁

        由于对整个方法加锁的设计效率太低,我们对这种方式进行优化:

        

     1 public class Singleton {
     2     //一个静态实例
     3     private static Singleton singleton;
     4     //私有构造方法
     5     private Singleton(){
     6         
     7     }
     8     //提供一个公共静态方法来获取一个实例
     9     public static Singleton getInstance(){
    10         
    11         if(singleton == null ){
    12             
    13             synchronized(Singleton.class){
    14                 
    15                 if(singleton == null){
    16                     
    17                     singleton = new Singleton();
    18                     
    19                 }
    20             }
    21         }
    22         
    23         return singleton;
    24         
    25     }
    26 }

        跟上面那种糟糕的设计相比,这种方式就好太多了。因为这里只有当singleton为null时才进行同步,当实例已经存在时直接返回,这样就节省了无谓的等待时间,提高了效率。注意在同步块中,我们再次判断了singleton是否为空,下面解释下为什么要这么做。假设我们去掉这个判断条件,有这样一种情况,当两个线程同时进入if语句,第一个线程t1获得线程锁执行实例创建语句并返回一个实例,接着第二个线程t2获得线程锁,如果这里没有实例是否为空的判断条件,t2也会执行下面的语句返回另一个实例,这样就产生了多个实例。因此这里必须要判断实例是否为空,如果已经存在就直接返回,不会再去创建实例了。这种方式既保证了线程安全,也改善了程序的执行效率。

        第三种方式:静态内部类

        

     1 public class Singleton {
     2     //静态内部类
     3     private static class SingletonHolder{
     4         private static Singleton singleton = new Singleton();
     5     }
     6     //私有构造方法
     7     private Singleton(){
     8         
     9     }
    10     //提供一个公共静态方法来获取一个实例
    11     public static Singleton getInstance(){
    12         
    13         return SingletonHolder.singleton;
    14         
    15     }
    16 }

        这种方式利用了JVM的类加载机制,保证了多线程环境下只会生成一个实例。当某个线程访问getInstance()方法时,执行语句访问内部类SingletonHolder的静态属性singleton,这也就是说当前类主动使用了改静态属性,JVM会加载内部类并初始化内部类的静态属性singleton,在这个初始化过程中,其他的线程是无法访问该静态变量的,这是JVM内部帮我们做的同步,我们无须担心多线程问题,并且这个静态属性只会初始化一次,因此singleton是单例的。

        第四种方式:饿汉式

        

     1 public class Singleton {
     2     //一个静态实例
     3     private static Singleton singleton = new Singleton();
     4     //私有构造方法
     5     private Singleton(){
     6         
     7     }
     8     //提供一个公共静态方法来获取一个实例
     9     public static Singleton getInstance(){
    10         
    11         return singleton;
    12         
    13     }
    14 }

        这种方式也是利用了JVM的类加载机制,在单例类被加载时就初始化一个静态实例,因此这种方式也是线程安全的。这种方式存在的问题就是,一旦Singleton类被加载就会产生一个静态实例,而类被加载的原因有很多种,事实上我们可能从始至终都没有使用这个实例,这样会造成内存的浪费。在实际开发中,这个问题影响不大。

        以上内容介绍了几种常见的单例模式的实现方式,分析了在多线程情况下的处理方式, 在工作中可根据实际需要选择合适的实现方式。还有一种利用枚举来实现单例的方式,在工作中很少有人这样写过,不做探讨。

  • 相关阅读:
    UOJ168. 【UR #11】元旦老人与丛林
    luogu3308,LOJ 2196 [SDOI2014]LIS
    CF1349F2. Slime and Sequences (Hard Version)
    6210. wsm
    欧拉数学习小记
    CF1508F. Optimal Encoding
    CF1508C. Complete the MST
    联合省选2021 游记
    一. Docker介绍
    Elasticsearch
  • 原文地址:https://www.cnblogs.com/fangfuhai/p/6666850.html
Copyright © 2011-2022 走看看