Java设计模式之单例模式

本文深入解析单例模式的原理、应用场景、优缺点及其实现方式,包括饿汉式与懒汉式的区别,以及如何防御反射攻击,最后介绍了单例模式的扩展——上限的多例模式。


设计模式

1.单例模式

《设计模式之禅》中对单例模式的概述是我是皇帝我独苗。意思是皇帝只有一个,也就是说一个类只能产生一个对象,而对象的产生是通过new关键字完成的(当然也有其他方式,比如对象复制,反射等)。当通过new关键字创建对象时,会根据输入的参数调用相应的构造函数,所以如果我们把构造函数设置为private私有访问权限就可以禁止外部创建对象了

1.1.单例模式的定义

单例模式是一个比较简单的模式,其定义如下:

Ensure a class has only one instance, and provide a global piont of class to it.(确保某一个类只有一个实例,而且自行实例化并像整个系统提供这个实例。)

Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())。

1.2.单例模式常见的应用场景

1.Windows的Task Manager(任务管理器)就是典型的单利应用。在打开任务管理器时,一次只能打开一个任务管理器。

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

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

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

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

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

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

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

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

1.3.单例模式的优缺点

优点:

1.由于单例模式在内存中只有一个实例,减少内存开支,特别是一个对象需要频繁地创建或销毁时性能又无法优化,单例模式就非常明显了

2.由于单例模式只生成一个实例,所以,减少系统的性能开销,当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

3.单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只是一个实例存在内存中,避免对同一个资源文件的同时写操作。

4.单例模式可以在系统设置全局访问点,优化个共享资源访问,例如,可以设计一个单例类,负责所有数据表的映射处理。

缺点:

1.单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。(单例模式不能增加接口是因为接口对单例模式是没有任何意义的,它要求”自行实例化“,并且提供单一实例、接口或者抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断)

2.单例对象如果持有Context,那么很容易引起内存泄露,此时需要注意传递给单例对象的Context对象最好是Application Context。

3.单例模式对测试是不利的。在并行开发环境下,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

4.单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,=而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

1.4.单例模式的代码实现

单例模式可以分为懒汉式饿汉式两种

饿汉式:

在程序启动或单例模式类被加载的时候,单例模式实例就已经被创建。

//单例设计模式饿汉模式
public class Singleton{
    private final static Singleton instance = new Singleton();
    //私有默认构造方法
    private Singleton(){}
    //静态工厂方法
    public static Singleton getInstance(){
        return instance;
    }
}

将饿汉式单例模式进行以下修改,可以简单的实现线程安全

//单例设计模式饿汉式简单的实现线程安全
public class Singleton{
    //私有默认构造方法
    private Singleton(){}
    //静态内部类,该类内部的实例与外部的实例没有绑定关系,而且只有在调用的时候才会装载,从而实现了延迟加载
    private static class SingletonInner{
        //静态初始化,用JVM来保证线程的安全
        private final static Singleton instance = new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonInner.instance;
    }
}

当getInstance方法第一次被调用的时候,它第一次读取SingletonInner.instance,导致SingletonInner类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并有虚拟机来保证它的线程安全性。

这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化,并没有增加任何访问成本。

如果单例模式实例在系统中经常会被用到,饿汉式是一个不错的选择

懒汉式:

当程序第一次访问单例模式实例时才进行创建

//单例模式简单的懒汉模式
public class Singleton{
    private static Singleton instance = null;
    //私有默认构造方法
    private Singleton(){}
    //静态工厂方法
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

该单例模式在低并发的情况下尚不会出现问题,若系统压力增大,并发量增加是则可能在内存中出现多个实例,破坏了最初的预期。

原因如下:

如果一个线程A执行到instance = new Singleton(),但还没有获得对象(对象初始化是需要时间的),第二个线程B也在执行,执行到(instance == null)判断,那么线程B获得判断条件也是真,于是继续运行下去,线程A获得一个对象,线程B也获得一个对象,在内存中就出现两个对象。

解决线程不安全的方法有很多,可以在getInstance方法前加synchronized关键字,也可以在getInstance方法内增加synchronized来实现,如下:

//单例模式懒汉式简单的实现线程安全
public class Singleton{
    private static Singleton instance = null;
    //私有默认构造方法
    private Singleton(){}
    //静态工厂加synchronized锁,实现线程安全
    public static synchronized Singleton getInstance(){
        if(instance == null){
            //A --->线程
           //B --->线程
            instance = new Singleton();
        }
        return instance;
    }
}

问题:当S为共享数据,可能会并发的访问getInstance方法,当多线程访问时,

一个A线程进来后,可能调用了sleep()方法在这里沉睡,一个B线程也可能在沉睡,当A线程醒来后,就会new一个对象,B线程醒来也会new一个对象;这样就不符合单例模式的特点了。

//单例设计模式双重检查加锁懒汉模式
public class Singleton{
    private static Singleton instance = null;
    //默认私有构造方法
    private Singleton(){}
     //静态工厂
    public static Singleton getInstance(){
        //先检查实例是否存在,如果不存在,进入下面的同步块
        if(instance == null){
            synchronized(Singleton.class){
                //再次检查实例是否存在,如果不存在才创建实例
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

按照《高效Java第二版》中的说法:单元素的枚举类型已经成为实现Singleton的最佳方法,用枚举来实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可

public enum Singleton{
    //定义一个枚举元素,它就代表了Singleton的实例
    singletonInstance;
    //单例可以有自己的操作
    public void SingleOperation(){
        //功能处理
    }
}
1.5.通过反射攻击单例以及改进
public class Test{
    public static void main(String[] args) throws Exception{
        Singleton s1 = Singleton.getInstance();
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2 = constructor.newInstance();
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());
    }
}

上述s1,s2的hashcode值是不同的,表明s1,s2是两个不同的实例。

通过反射获得单例类的构造函数,由于该构造函数是private的。通过setAccessible(true)指示反射的对象在使用时应该消除Java语言访问检查,使得私有的构造函数能够被访问,这样使得单例模式失效。

如果要抵御这种攻击,要防止构造函数被成功调用两次。需要在构造函数中对实例化次数进行统计,大于一次就排除异常。

public class Singleton{
    private static int count = 0;
    private static Singleton instance = null;
    private Singleton(){
        synchronized(Singleton.class){
            if(count > 0){
                throw new RuntimeException("创建了两个实例");
            }
            count++;
        }
    }
     public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
} 
1.6.单例模式的扩展

如果要求一个类只能产生两三个对象?该怎么实现?

《设计模式之禅》中以一个朝代同一时代出现两个皇帝来做比喻。这样的话,大臣参拜的时候就会出现今天跟昨天参拜的皇帝是不同的人的问题。代码如下:

public class Emperor{
    //定义最多能产生的实例数量
    private static int maxNumOfEmperor = 2;
    //每个皇帝都有名字,使用一个ArrayList来容纳,每个对象的私有属性
    private static ArrayList<String> nameList = new ArrayList<String>();
    //定义一个列表,容纳所有的皇帝实例
    private static ArrayList<Emperor> emperorList = new ArrayList<Emperor>();
    //当前皇帝序列号
    private static int countNumOfEmperor = 0;
    //产生所有的对象
    static{
        for(int i = 0 ; i < maxNumOfEmperor ; i++){
            emperorList.add(new Emperor(""+(i+1)+""));
        }
    }
    private Emperor(){
        //世俗道德约束你,目的就是不产生第二个皇帝
    }
    //传入皇帝名称,建立一个皇帝对象
    private Emperor(String name){
        nameList.add(name);
    }
    //随机获得一个皇帝对象
    public static Emperor getInstance(){
        Random random = new Random();
        //随便拉出一个皇帝,只要是个精神领袖就成
        countNumOfEmperor = random.nextInt(maxNumOfEmperor);
        return emperorList.get(countNumOfEmperor);
    }
    //皇帝发话了
    public static void say(){
        System.out.println(nameList.get(countNumOfEmperor));
    }
}
public class Minister {
    public static void main(String[] args) {
        //定义5个大臣
        int ministerNum =5;
            for(int i=0;i<ministerNum;i++){
                Emperor emperor = Emperor.getInstance();
                System.out.print("第"+(i+1)+"个大臣参拜的是: ");
                emperor.say();
            }
    }
}

结果:

第1个大臣参拜的是: 皇1帝

第2个大臣参拜的是: 皇2帝

第3个大臣参拜的是: 皇1帝

第4个大臣参拜的是: 皇1帝

第5个大臣参拜的是: 皇2帝

这种需要产生固定数量对象的模式叫做上限的多例模式,它是单例模式的一种扩展,采用有上限的单例模式,我们可以在设计时决定在内存中有多个实例,方便系统进行扩展,修正单例可能存在的性能问题,提供系统的响应速度。例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的reader实例,然后在需要读取文件时就可以快速响应。

 

 

 

 

转载于:https://www.cnblogs.com/canglg/p/10430617.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值