单例模式(含多线程处理)

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

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

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

     第一种方式:懒汉式单例并发

     

 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 }

 

    在不考虑并发的状况下,这是标准的单例构造方式,它经过如下几个要点来保证咱们得到的实例是单一的。ide

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

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

    三、提供公共的静态方法来返回一个实例, 把这个方法设置为静态的是有缘由的,由于这样咱们能够经过类名来直接调用此方法(此时咱们尚未得到实例,没法经过实例来调用方法),而非静态的方法必须经过实例来调用,所以这里咱们要把它声明为静态的方法经过类名来调用;测试

    四、判断只有持有的静态实例为null时才经过构造方法产生一个实例,不然直接返回。优化

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

    

 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类被加载就会产生一个静态实例,而类被加载的缘由有不少种,事实上咱们可能从始至终都没有使用这个实例,这样会形成内存的浪费。在实际开发中,这个问题影响不大。

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

相关文章
相关标签/搜索