单例模式
单例模式(Singleleton Pattern) 是简单的一种设计模式。
1单例模式的定义
单例模式的英文原文是:
Ensure a class has only one instance,and provide a global point of access to it.
意思是:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式的主要作用是确保一个类只有一个实例存在。单例模式可以用在建立目录、数据库连接等需要单线程操作的场合,用于实现对系统资源的控制。
由于Java的语言特点,使得在Java中实现单例模式通常有两种表现形式。
- 饿汉式单例模式:类加载时,就进行对象实例化;
- 懒汉式单例模式:第一次引用类时,才进行对象实例化。
1.饿汉式单例类
饿汉式单例模式-类图
饿汉式源码
package com.eric.创建型模式.单例模式.懒汉式;
/**
* @author Eric
* @ProjectName my_design_23
* @description 饿汉式单例模式
* @CreateTime 2020-11-25 15:42:47
*/
public class Singleton {
private static Singleton m_instance = new Singleton();
//构造方法私有,保证外界无法直接实例化
private Singleton(){}
//通过该方法获取实例对象
public static Singleton getInstance(){
return m_instance;
}
}
从上述代码中可以看到,在类被加载时,静态变量m_instance会被初始化,此时类的私有构造器会被调用,单例类的唯一实例就被创建出来了。单例类中最重要的特点是类的构造函数是私有的,从而避免外界利用构造函数直接创建出任意多的实例。另外需要注意的是,由于构造函数是私有的,因此该类不能被继承。
2懒汉式单例类
懒汉式单例类与饿汉式单例类相同的是,类的构造函数是私有的;不同的是,懒汉式单例类在加载不会将自己实例化,而是在第一次被调用时将自己实例化。(去掉 synchronized就是线程不安全的了)
懒汉式单例
package com.eric.创建型模式.单例模式.饿汉式;
/**
* @author Eric
* @ProjectName my_design_23
* @description 懒汉式
* @CreateTime 2020-11-25 16:55:12
*/
public class Singleton {
private static Singleton _instance = null;
//构造方法私有,保证外界无法直接实例化
private Singleton(){}
//方法同步
synchronized public static Singleton getInstance(){
if(_instance == null){
_instance = new Singleton();
}
return _instance;
}
}
上述代码中,懒汉式单例模式中对静态方法getInstance()进行同步,以确保多线程环境下只创建一个实例,例如,如果getInstance方法未被同步,并且线程A和线程B同时调用此方法,则执行if(_instance == null)语句都为真,那么线程A和线程B都会创建一个对象,在内存中就会出现两个对象,这样就违反了单例模式;但使用synchronized关键字同步后,则不会出现此种情况。
饿汉式单例模式与懒汉式单例模式的区别。
- 饿汉式单例模式在被加载时实例化,而懒汉式单例模式在第一次引用时被实例化。
- 从资源利用效率上,饿汉式差一些;但从速度和执行时间来看,饿汉式要好一些。
- 饿汉式单例模式可以在Java中实现,但不易在C++内实现。实际上饿汉式单例模式更符合Java语言本身的特点。
2单例模式应用
1.单例模式优点
- 由于单例模式在内存中只有一个实例,减少了内存的开销,特别是一个对象需要频繁的创建、销毁,而且创建或销毁的性能有无法优化时,单例模式的优势就非常明显了。
- 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多资源时,如读取配置、产生其他依赖对象时,则可以通过在启用时直接产生一个单例对象,然后永久驻留内存的方式来解决。
- 单例模式可以避免多重占用,例如一个写文件动作,由于只有一个实例存在于内存中,避免了对同一个资源文件的同时操作。
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有数据库表的映射处理。
2.单例模式的缺点
- 单例模式无法创建子类,扩展困难。若要扩展,除了修改代码基本没有第二种途径。
- 单例模式对测试不利。在并行开发环境中,如果采用单例模式的类没有完成,是不能进行测试的;单例模式的类通常不会实现接口,这也妨碍了使用mock方式虚拟一个对象。
- 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不应该关心它是否是单例的,是不是要用单例模式取决于环境,单例模式把“要单例”和业务逻辑融合在一起。
注意 单元测试时经常会采用stub和mock方式,这两种都可以对系统模块或单元进行隔离,通过创建虚拟的对象来模拟真实场景,一遍对测试对象进行测试工作。(stub和mock看其他资料)
3.单例模式的使用场景
在一个系统中,如果要求一个类有且仅有一个实例,当出现多个实例时就会造成不良反应,则此时可以采用单例模式。
- 要求生成唯一序列号的环境。
- 在整个项目中需要一个共享访问点或共享数据;例如Web页面上的计数器,可以不用吧每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保线程是安全的。
- 创建一个对象需要消耗的资源过多时,如访问IO和数据库等资源。
- 需要定义大量的静态常量和静态方法(如工具类的环境),可以采用单例模式(也乐意采用直接声明为static的方式)。
单例模式是23个模式中比较简单的模式,应用也非常广泛,如在Spring框架中,每个Bean默认就是单例的;Java基础类中的java.lang.Runtime类也采用了单例模式,其getRuntime()方法返回了唯一实例。
4.使用单例模式的注意事项
根据功能,单例类可以分为有状态单例模式和无状态模式。
- 有状态单例类:一个有状态单例模式的对象一般是可变的,通常当做状态库使用。例如,给系统提供一个唯一的序列号。
- 无状态单例类:无状态的单例模式的对象是不变的,通常用来提供工具性的功能方法。例如,IO或数据库库访问等。
因为单例类具有状态,所以在使用时应注意以下两点
- 单例类仅局限与一个JVM,因此当多个JVM的分布式时,这个单例类就会在多个JVM中被实例化,造成多个单例对象的出现。如果是无状态的单例类,则没有问题,因为这些单例对象是没有区别的。如果是有状态的但单例类,则会出现问题。如,给系统提供一个唯一序列号时,序列号不唯一,可能出现多次。因IC,在任何使用EJB、RMI和JINI技术的分布式系统中,应当避免使用有状态的单例类。
- 同一个JVM中会有多个类加载器,当两个类加载器同时加载同一个类时,会出现两个实例,此时也应尽量避免使用有状态的单例类。
另外,使用单例模式时,需要注意序列化和克隆对实例唯一性的影响。如果一个单例的类实现了Serializable或Cloneable接口,则有可能被反序列化或克隆出一个新的实例来,从而破坏了“唯一实例”的要求,因此,通常单例类不需要实现Serializable或Cloneable接口。
3单例模式实例
例:使用单例模式记录访问次数
GlobalNum.java
package com.eric.创建型模式.单例模式.例1;
/**
* @author Eric
* @ProjectName my_design_23
* @description 全局计数器
* @CreateTime 2020-11-25 18:51:54
*/
public class GlobalNum {
private static GlobalNum gn = new GlobalNum();
private int num = 0;
public static GlobalNum getInstance(){
return gn;
}
public synchronized int getNum(){
return ++num;
}
}
上述代码中创建一个饿汉式单例类GlobalNum,其中getNum()方法用于返回访问次数,并且使用synchronized对该方法进行线程同步。
NumThread.java
package com.eric.创建型模式.单例模式.例1;
/**
* @author Eric
* @ProjectName my_design_23
* @description 线程类
* @CreateTime 2020-11-25 18:56:57
*/
public class NumThread extends Thread{
private String threadName;
public NumThread(String name){
threadName = name;
}
//重写线程的run方法(线程任务)
@Override
public void run() {
GlobalNum gnObj = GlobalNum.getInstance();
//循环访问,输出访问次数
for (int i = 0; i < 5; i++) {
System.out.println(threadName+"第"+gnObj.getNum()+"次访问!");
try{
this.sleep(1000);//线程休眠1s
}catch (Exception e){
e.printStackTrace();
}
}
}
}
SingleDemo.java
package com.eric.创建型模式.单例模式.例1;
/**
* @author Eric
* @ProjectName my_design_23
* @description 测试单例模式
* @CreateTime 2020-11-25 18:54:53
*/
public class SingleDemo {
//测试单例模式
public static void main(String[] args) {
//创建线程A
NumThread a = new NumThread("线程A");
//创建线程B
NumThread b = new NumThread("线程B");
//启动线程
a.start();
b.start();
}
}
上述代码在主程序中创建两个子线程,通过这两个子线程演示对单例模式下唯一实例的访问。因为GlobalNum的对象是单例的,所以能够统一地对县城访问次数进行统计。
由于上述代码是多线程的,运行结果每次都有可能出现不同,可能的运行结果。
4其他几种单例模式的实现(重要!)
双检锁/双重校验锁(DCL,即double-checked Locking)
JDK版本:JDK1.5起
是否Lazy初始化:是
是否线程安全:是
实现难度:较复杂
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance()的性能对应用程序很关键。
package com.eric.创建型模式.单例模式.双重校验锁;
/**
* @author Eric
* @ProjectName my_design_23
* @description 双检锁/双重校验锁
* @CreateTime 2020-11-25 19:19:58
*/
public class Singleton {
private volatile static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
登记式/静态内部类
是否Lazy初始化:是
是否线程安全:是
实现难度:一般
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了ClassLoader机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是:饿汉式只要Singleton类被装载了,那么instance就会被实例化(没有达到Lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。
因为SingletonHolder类没有被主动使用,只有通过显式调用getInstance()方法时,才会显式装载SingletonHolder类,从而实例化instance。可以想象,如果实例化instance很消耗资源,所以想让他延迟加载,另一方面,又不希望在Singleton类加载时就实例化,因为不能确保Singleton类还可能在其他地方被主动使用从而被加载,那么这时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式就显得更合理。
package com.eric.创建型模式.单例模式.登记式;
/**
* @author Eric
* @ProjectName my_design_23
* @description 登记式/静态内部类-----单例模式
* @CreateTime 2020-11-25 19:51:40
*/
public class Singleton {
//静态内部类SingletonHolder
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
//私有构造器
private Singleton (){}
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
枚举
JDK版本:JDK1.5起
是否Lazy初始化:否
是否线程安全:是
实现难度:易
描述:还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是Effective Java 作者Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止序列化重新创建新的对象,绝对防止多次实例化。不过由于JDK1.5之后才加入enum特性,用这种方式写,不免让人感到生疏,实际工作中,也很少用。
不能通过reflection attack来调用私有构造方法。
package com.eric.创建型模式.单例模式.枚举式;
/**
* @author Eric
* @ProjectName my_design_23
* @description 枚举方式的单例模式
* @CreateTime 2020-11-25 20:02:05
*/
public enum Singleton {
INSTANCE;
public void whateverMethod(){
System.out.println("电脑开始做起了奇奇怪怪的事情...");
}
}
[注]:
一般情况下,不建议使用懒汉式,建议使用饿汉式。只有在明确时限lazy loading效果时,才会使用登记方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊需求,可以考虑使用双检锁方式。