设计模式之单例模式

本文详细探讨了设计模式中的单例模式,包括其意图、结构与参与者、优点和实现方式。单例模式确保类只有一个实例并提供全局访问点,常用于数据库连接池和文件管理器等场景。文章介绍了懒汉模式和饿汉模式两种常见的实现策略,并讨论了通过子类化单例以实现更具灵活性的配置。此外,还提到了单例模式在多线程环境下的线程同步问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

       学习了这么久的设计模式,决定做个总结。看到优快云博客上也有作者写过一天一个设计模式的,哈哈哈,我惭愧难过,做不到一天一个,那就两天一个设计模式吧。我力求把每一个模式总结得全面,除了基本原理以外,每个模式的应用场景,以及应该注意的问题等将会有介绍。在此,先特别感谢GOF的《设计模式—可复用面向对象软件的基础》这本书,真的是经典之作,也特别感谢其他已经做过设计模式总结的网友们,有了你们的知识分享,我才会看得更远更高。就暂时按照我们经常使用的模式的先后顺序来吧,声明下哈,不是说先后顺序就一定是使用频率的大小~

  1 意图

   保证一个类仅有一个实例,且能自行实例化,并提供一个访问它的全局访问点。

  2 结构及参与者

  

   参与者Singleton:

  —— 定义一个Instance()的操作,允许客户访问它的唯一实例。Instance是一个类操作,即C++/java中的static成员函数。

  ——负责创建它自己的唯一实例。

3 优点

1)对唯一实例受控的访问。Singleton类只有唯一的实例,因此可以严格地控制客户怎样以及何时访问它。

2)缩小名空间。Singleton模式是对全局变量的一种改进,可避免那些存储唯一实例的全局变量污染名空间。

3)允许对操作和表示的精化。Singleton可以有子类,客户可以使用所需要的类的实例在运行时刻配置。关于此点,不好理解的,请参考实现部分的"创建Singleton的子类"一节。

4)允许可变数目的实例。允许Singleton类的多个实例,其实就演化为多例模式了。关于此观点,请参考“单例模式的扩展——多例模式”这部分。

5)比类操作更灵活。另一种封装单件的方法是使用类操作,即静态方法,但是C++中的静态成员函数不是虚函数,对于java,则天生多态。

4 实现

1)保证一个唯一的实例

     下面的代码展示了最常见的Singleton实现方式。Singleton的实现一般分为懒汉模式和饿汉模式。所谓的懒汉模式,就是将实例化工作在需要实例的时候进行,而饿汉模式则是在类加载的时候就进行实例化。需要说明的是,以下的实现方式并没有考虑线程同步的问题,关于Singleton模式下的线程同步将会在第6部分中做介绍。

 ——懒汉模式的Singleton

public class Singleton {
	private static Singleton uniqueInstance;
	
	private Singleton (){
		System.out.println("Singleton constructor.");
	}
	
	public static Singleton instance(){
		if(uniqueInstance==null){
			uniqueInstance=new Singleton();
		}
		return uniqueInstance;
	}
}

——饿汉模式的Singleton

public class Singleton {
	private static Singleton uniqueInstance=new Singleton();
	
	private Singleton (){
		System.out.println("Singleton constructor.");
	}
	
	public static Singleton instance(){
		return uniqueInstance;
	}
}

       从上面的程序,我们可以看出,懒汉模式和饿汉模式的最大区别在于实例化的时刻不一样,那么也正是实例化的时刻不同,决定了它们在应用场景的区别。首先,懒汉模式下存在线程安全的问题,假设有两个线程A和线程B,在某一时刻,A和B都运行到判断 uniqueInstance是否为空的语句,此时,不难想象,程序中无法保证唯一的Singleton实例了;其次,饿汉模式适合于待创建的对象需要较多的资源和时间的时候,而懒汉模式则适合待创建的对象所需资源和时间较少的场景。

2)创建Singleton的子类

     创建Singleton的子类就是使用子类的实例进行初始化,较为简单的就是在instance()操作中决定使用哪一类单件。一种较为灵活的实现方式就是单件注册表(Registry of Singleton )。注册表的实质就是建立字符串名字和单件之间的映射,当需要一个单件的时候,参考注册表,根据名字请求单件。下面的程序展示了单件注册表的实现过程。

import java.util.*;

public class SingletonB  {
	private static Hashtable registry = new Hashtable();
    
    public static void Register(String name, SingletonB aInstance) {
        registry.put(name, aInstance);
    }
    public static SingletonB GetInstance(String name) {
        return LookUp(name);
    }
    
    private static SingletonB LookUp(String name) {
        return (SingletonB)registry.get(name);
    }
}
public class SubSingletonB extends SingletonB {
    public static boolean instanceFlag = false; //true if 1 instance
    
    public SubSingletonB() throws SingletonException {
        if(instanceFlag) {
            throw new SingletonException("Only can create a instance !");
        } else {
            instanceFlag = true;
            super.Register("Sub1", this);
        }
    }
}


public class SingletonException extends RuntimeException {
    public SingletonException() {
        super();
    }
    
    public SingletonException(String s) {
        super(s);
    }
}
public class TestB  {
    public static void main(String[] args) {
        // First we get a instance from SingletonB
        SingletonB instance1 = SingletonB.GetInstance("Sub1");
        if(instance1 == null) {
            System.out.println("There is no such instance in registry !");
        } else {
            System.out.println(instance1.getClass());
        }
        
        // Then we register a new instance
        try {
            SingletonB instance2 = new SubSingletonB();
            System.out.println("We had created a new instance named \"Sub1\" now");
        } catch (SingletonException e) {
            System.out.println(e.getMessage());
        }
        
        // To get instance again
        instance1 = SingletonB.GetInstance("Sub1");
        if(instance1 == null) {
            System.out.println("There is no such instance in registry !");
        } else {
            System.out.println(instance1.getClass());
        }

        // Finally we create a new instance again
        try {
            SingletonB instance3 = new SubSingletonB();
            System.out.println("We had created a new instance named \"Sub1\" now");
        } catch (SingletonException e) {
            System.out.println(e.getMessage());
        }
    }
}
5 单例模式的扩展——多例模式
  单例模式的扩展,有上限的多例模式。多例模式或多例类有以下的特点:
1、多例类可以有多个实例。
2、多例类必须自己创建,管理自己的实例,并向外界提供自己的实例。
 在《设计模式之禅》这本书中展示了多例模式的实例,见下面的程序代码:

public class Emperor {
	private static List<String> nameList = new ArrayList<String>();
	private static List<Emperor> empList = new ArrayList<Emperor>();
	private static int curEmpNum = 0;
	private static int maxEmpNum = 3;


	private Emperor() {


	}


	private Emperor(String name) {
		nameList.add(name);
	}


	static {
		for (int i = 0; i < maxEmpNum; i++) {
			empList.add(new Emperor("皇帝:" + (i + 1)));
		}
	}


	public static Emperor getInstance() {
		Random random = new Random();
		int index = random.nextInt(maxEmpNum);
		return empList.get(index);
	}
}

多例模式适合于在设计时决定内存中实例个数的场景。
6  应用
  单例模式的应用总结起来主要有以下几方面:
1)控制实例产生的数量。若对象频繁地创建和撤销,性能无法优化时受,或者对象的产生需要较多的资源和时间的时候,关于后一方面,其实就是饿汉模式。比如,数据库连接池的设计一般采用单例模式。数据库连接是一种数据库资源。软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的。当然,使用数据库连接池还有很多其它的好处,可以屏蔽不同数据数据库之间的差异,实现系统对数据库的低度耦合,具有高可复用性,还能方便对数据库连接的管理等等。数据库连接池属于重量级资源,一个应用中只需要保留一份即可,既节省了资源又方便管理。所以数据库连接池采用单例模式进行设计会是一个非常好的选择。
2)需要一个共享访问点或共享数据。
再比如,每台计算机可以有若干个打印机,如果每一个进程或者线程都独立地使用打印机资源的话,那么我们打印出来的结果就有可能既包含这个打印任务的一部分,又包含另外一个打印任务的一部分。所以,大多数的操作系统最终为打印任务设计了一个单例模式的假脱机服务Printer Spooler,所有的打印任务都需要通过假脱机服务进行。
3)优化和资源访问。
4)避免对资源的多重占用,如写文件动作。
在我们日常使用的在Windows中也有不少单例模式设计的组件,象常用的文件管理器。由于Windows操作系统是一个典型的多进程多线程系统,那么在创建或者删除某个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象。采用单例模式设计的文件管理器就可以完美的解决这个问题,所有的文件操作都必须通过唯一的实例进行,这样就不会产生混乱的现象。
7 同步问题
   前面提到了懒汉模式,在懒汉模式中我们对线程安全提出了质疑。因文章较长,怕大家看得太累,关于懒汉模式的线程安全问题,请见博文《单例模式以及双检锁DCL》







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值