【源码分析设计模式 1】JDK中的单例模式

 更新时间:2022-12-27   作者:  

一、基本介绍

单例模式比较简单,可以说没有复杂的调用和接口的设计,就是一个简单的类,只是要求这个类只生成一个对象,无论什么时候都要保证这一点,因此只能生成一个实例的模式就是单例模式。


1、单例模式要素

私有构造方法;

私有静态引用指向自己实例 ;

以自己实例为返回值的公有静态方法;

二、类的加载

类的加载是通过类加载器(Classloader)完成的,它既可以是饿汉式加载类,也可以是懒汉式加载,这跟不同的JVM实现有关。加载完类后,类的初始化就会发生,如果是对一个类的主动使用就会初始化对象,对类的被动使用不会对类进行初始化,比如final修饰的静态变量如果能在编译时就确定变量的取值,会被当做常量,作为对一个类的被动使用不会导致类的初始化。以下情况类被初始化:


类初始化的一些规则:


1、类从顶到底的顺序初始化,所以声明在顶部的字段遭遇底部的字段初始化;

2、超类早于子类和衍生类的初始化;

3、如果类的初始化是由于访问静态域而触发,那么只能声明静态域的类才被初始化,而不会触发超类的初始化或者子类的初始化,即使静态域被子类或子接口或者它的实现类锁引用;

4、接口初始化不会导致父接口的初始化;

5、静态域的初始化时在类的静态初始化期间,非静态域的初始化是在类的实例创建期间,这意味着静态域初始化在非静态域之前;

6、非静态域通过构造器初始化,子类在做任何初始化之前构造器会先调用父类的构造器,它保证了父类非静态或实例变量初始化早于子类;


三、单例模式的优缺点

1、优点

(1)在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例;


(2)单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性;


(3)提供了对唯一实例的受控访问;


(4)由于在系统内存中只存在一个对象,因此可以 节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能;


(5)允许可变数目的实例;


(6)避免对共享资源的多重占用;


2、缺点

(1)不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态;


(2)由于单例模式中没有抽象层,因此单例类的扩展有很大的困难;


(3)单例类的职责过重,在一定程度上违背了“单一职责原则”;


(4)滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失;


四、单例模式的使用场景

单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。


1、需要频繁实例化然后销毁的对象;


2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象;


3、有状态的工具类对象;


4、频繁访问数据库或文件的对象;


五、单例模式的经典场景

1、资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置;


2、控制资源的情况下,方便资源之间的互相通信。如线程池等;


3、应用场景举例:

(1)外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件 


(2)Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~ 


(3)windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。 


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


(5)应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。 


(6) Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。 


(7)数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。 


(8)多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。  


(9)操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。 


(10)HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例。


六、单例模式的实现方法


1、饿汉式单例

public class Singleton {

    private Singleton() {}

    private static final Singleton single = new Singleton();

    public static Singleton getInstance() {

        return single;

    }

    //单例模式,构造器要私有化

    private SingletonTest {

    }

}

因为这本身就是static修饰的方法,所以是在类加载的时候被创建,后期不会再改变,所以线程是安全的。


2、懒汉式单例

public class SingletonTest {
    public static SingletonTest singleton = null;
    public static SingletonTest getInstance(){
        if(singleton == null){
            singleton = new SingletonTest();
        }
        return singleton;
    }
    //单例模式,构造器要私有化
    private SingletonTest {
    }
}

懒汉式方法总是会出现这样或那样的问题的,因为考虑到了多线程机制,实现起来比较麻烦,并且还会出现问题,就算是使用了一定的解救办法(同步、加锁、双重判断)的办法,性能还是被损耗了,因此懒汉式方法的不推荐使用。

3、静态内部类实现单例

package designMode.singleton;
 
public class Singleton {
    private static class SingleTonHoler{
        private static Singleton INSTANCE = new Singleton();
    }
 
    public static Singleton getInstance(){
        return SingleTonHoler.INSTANCE;
    }
 
    //单例模式,构造器要私有化
    private SingletonTest {
    }
}

(1)静态内部类实现单例的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。即当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。


(2)当getInstance()方法被调用时,SingleTonHoler才在SingleTon的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。那么INSTANCE在创建过程中又是如何保证线程安全的呢?在《深入理解java虚拟机》中,有这么一句话:


虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其它线程都需要阻塞等待,直到活动线程执行()方法完毕。如果一个类的()方法中有耗时很长的操作,就有可能造成多个进程阻塞(需要注意的是,其它线程虽然会被阻塞,但如果执行()方法后,其它线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。)在实际应用中,这种阻塞往往是隐蔽的。

()方法简介:


1、先理解类初始化阶段的含义:该阶段负责为类变量赋予正确的初始值,是一个类或接口被首次使用前的最后一项工作。


2、()方法的执行时期:类初始化阶段(该方法只能被JVM调用,专门承担类变量的初始化工作)。


3、()方法的内容:所有的类变量初始化语句和类型的静态初始化器。


4、类的初始化时机:


首次创建某个类的新实例时(new,反射,克隆,反序列化);

首次调用某个类的静态方法时;

首次使用某个类或接口的静态字段或对该字段(final字段除外)赋值时;

首次调用java的某些反射方法时;

首次初始化某个类的子类时;

首次在虚拟机启动时某个含有main()方法的那个启动类。

5、注意:并非所有的类都拥有一个()方法,满足下列条件之一的类不会拥有()方法:


该类没有声明任何类变量,也没有静态初始化语句;

该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化;

该类仅包含静态final变量的类变量初始化语句,并且类变量初始化语句是编译时常量表达式。

故而,可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。


那么,是不是可以说静态内部类实现单例模式就是完美的了呢?其实不然,静态内部类实现单例也有一个致命的缺点,就是传参的问题,由于静态内部类的形式创建单例,故而无法传递参数进去,例如Contxt这种参数,所以我们创建单例时,可以根据实际情况,进行斟酌。


4、双检锁/双重校验锁

描述:采用双锁机制,安全且在多线程情况下能保持高性能。

package designMode;
 
public class Singleton {
    private volatile static Singleton singleton;
    public static Singleton getSingleton(){
        if(singleton==null){
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    //构造函数私有化
    private Singleton(){
    }
}

这里的两次判断,第一判断:效率,第二判断:避免同步。之所以这样是因为避免加锁后,再次加锁。大大增强了执行效率。推荐使用!


5、枚举实现单例

(1)枚举单例(Enum Singleton)在Effective Java一书中提到,因为其功能完善,使用简洁,无偿的提供了序列化机制,在面对复杂的序列化或者反射攻击时依然可以绝对防止多次实例化等优点,被作者所推崇。


(2)枚举单例写法简单


如上文提到的DCL(double checked locking),实在是优点麻烦,枚举单例相对简单的多。下面这段代码就是声明枚举实例的通常做法,它可能还包含实例变量和实例方法,枚举单例是线程安全的。

————————————————

public enum  DataSourceEnum {
    DATASOURCE;
    private DBConnection connection = null;
    private DataSourceEnum(){
        connection = new DBConnection();
    }
    public DBConnection getConnection(){
        return connection;
    }
}

八、JDK中的单例模式

1、Runtime

Runtime类封装了Java运行时环境。每一个Java程序实际上都启动了一个JVM进程,那么每个JVM进程都对应一个Runtime实例,此实例由JVM为其实例化。每个Java应用程序都有一个Runtime实例,使应用程序能够与其运行的环境相连接。


由于Java是单进程的,所以,在一个JVM中,Runtime的实例应该只有一个。所以应该使用单例来实现。

————————————————

2、java.awt.Toolkit

懒汉式单例。不需要事先创建好,只要在第一次真正用到的时候再创建就可以了。因为很多时候并不常用Java的GUI和其中的对象。如果使用饿汉单例的话会影响JVM的启动速度。

九、总结

1、当一个类的对象只需要或者只可能有一个时,应该考虑单例模式。


2、如果一个类的实例应该在JVM初始化时被创建出来,应该考虑使用饿汉式。


3、如果一个类的实例不需要预先被创建,也许这个类的实例并不一定能用得上,也许这个类的实例创建过程比较耗费时间,也许就是真的没必要提前创建。那么应该考虑懒汉式。


4、在使用懒汉式单例的时候,应该考虑到线程的安全性问题。

————————————————