漫谈设计模式(一) —— 单例模式

1. 设计模式

设计模式(Design patterns)是一种解决特定类型问题的巧妙而具有深刻见解的方法,随着众多开发前辈的反复使用与推敲,成为软件领域的一个分支,为众多面向对象的软件开发人员所使用。

设计模式的目标是隔离代码中需要更改的部分,使得其优雅而易于维护且具备更好的可靠性。从实现目标的角度看,继承、组合都算是一种模式。

1995年,由Addison-wesley出版的《Design Patterns》(作者:Gamma,Helm,Johnson & Vlissides)中讲述了23中不同的设计模式,并将它们分为三个类别,分别为:

  1. 创建型(Creational):如何创建对象。通常涉及隔离创建对象的细节,使得代码不依赖于具体对象类型,将对象的创建和使用隔离。本文所要讲的单例模式就属于一种创建型模式。
  2. 结构型(Structural):描述如何用一系列对象的设计及构建来满足特定的项目需求,处理对象与其他对象连接的方式。用敲代码的现实例子来解释,则可以理解为,如何使用一系列代码块组合构建来形成一个可靠稳定的系统。
  3. 行为型(Behavioural):处理不同类型的行为,重点关注类和对象之间的相互作用,为不同对象划分责任和算法。

设计模式共有六大原则:

1. 开闭原则(Open Close Principle):对扩展开放,对修改关闭。

在程序需要进行拓展的时候,不去修改原有的代码。使程序的扩展性好,易于维护和升级。使用接口和抽象类可达到此效果。

2. 里氏代换原则(Liskov Substitution Principle)

LSP讲的是基类和子类的关系。只有当这种关系存在时,里氏代换关系才存在。

它是面向对象设计的基本原则之一。原则观点为:任何基类可以出现的地方,子类一定可以出现。

它是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。

它是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

3. 依赖倒转原则(Dependence Inversion Principle):面对接口编程,依赖于抽象而不依赖于实现类。

这个原则是开闭原则的基础。

4. 接口隔离原则(Interface Segregation Principle):使用多个隔离的接口,比使用单个接口要好。

降低类之间的耦合度。强调低依赖,低耦合。

5. 迪米特法则,又称最少知道原则(Demeter Principle):一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立

6. 组合优于继承:尽量使用合成/聚合的方式,而不是使用继承。

2. 单例模式

熟悉Spring的开发人员一定对Bean的作用域这个配置项不陌生,如下面的这句在开发手册中的配置示例:

<!-- the following is equivalent, though redundant (singleton scope is the default); using spring-beans-2.0.dtd -->
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>

其中的 scope="singleton"指的是在每个Spring IoC容器中一个bean定义对应一个对象实例,也其实就是单例。但这里的单例和我们所要说的java单例模式中的单例还不一样,下面是开发手册中的说明。

请注意Spring的singleton bean概念与“四人帮”(GoF)模式一书中定义的Singleton模式是完全不同的。经典的GoF Singleton模式中所谓的对象范围是指在每一个ClassLoader中指定class创建的实例有且仅有一个。把Spring的singleton作用域描述成一个container对应一个bean实例最为贴切。

前面的这段内容,其实只为了引出单例这个话题。单例模式(singleton pattern),是一些书籍以及开发人员都公认为java设计模式中最简单的一种。但你真的能够不费吹灰之力的写好单例模式吗?其内容或形式简单,但不应该被简单的对待。

如前文所述,单例模式是创建型模式中的一种。也亦如上文引用的Spring开发手册中说的,其作用就是确保一个类有且仅有一个实例。

这种模式涉及到一个类,这个类的构造器必须是私有的以防止其他类创建该类实例,同时还负责创建自己的对象,确保这个对象唯一通过某个特定方法对外传递给其它类或对象。

单例模式有多种实现方法,按照对象初始化和创建的时间顺序,可以分为“懒汉式”和“饿汉式”。所谓“懒汉式”,也即懒初始化,在首次使用时才创建实例;而相应的,“饿汉式”,也即不考虑懒初始化,在类被加载时就创建实例。

另外,为了满足单例的实际需求,就必须考虑线程安全问题。保证了线程安全才能保证单例,否则多个线程可能会创建出多个实例,就会与单例的需求相违背。

本文总结几种比较常见常用的方法。

2.1 简单实现

public class SingletonObject {
 
   //(1)创建自己的对象,最好是final
   private final static SingletonObject INSTANCE= new SingletonObject ();
 
   //(2)构造函数私有
   private SingletonObject (){}
 
   //(3)提供特定方法对外传递
   public static SingletonObject getInstance(){
      return INSTANCE;
   }
}

这在分类上属于饿汉式,在类装载的时候已经完成实例化,所以没有线程同步的问题,也即线程安全。

这是比较常用也是相对简单的一种形式。但如果使用这种方式,最好确定已经创建的实例一定会被使用,否则多少会造成内存资源的浪费

2.2 静态内部类实现

下面是《on java8》中的一个例子(部分删减)

interface Resource {
    int getValue();
    void setValue(int x);
}

final class Singleton {
    private static final class ResourceImpl implements Resource {
        private int i;
        private ResourceImpl(int i) {//... }
    }

    private static class ResourceHolder {
        private static Resource resource = new ResourceImpl(47);
    }
    public static Resource getResource() {
        return ResourceHolder.resource;
    }
}

乍一看,多了一个接口和实现类,将代码弄得复杂了些,但其实原理是一样的。内部私有的静态类ResourceHolder在首次引用前不会被加载,而JVM的工作方式决定了静态内部类是线程安全的。

这与下面的代码其实是基本一致的,只不过多了些实际应用的背景。

public class SingletonObject {  
   private SingletonObject(){}  

    private static class SingletonHolder {  
    	private static final SingletonObject INSTANCE = new SingletonObject();  
    }  
   
    public static final SingletonObject getInstance() {  
    	return SingletonHolder.INSTANCE;  
    }  
}

和上面一样,SingletonObject 被装载时,SingletonHolder作为静态内部类,在手册使用前不加载,所以INSTANCE未被初始化。只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类并创建 对象。

此类实现从分类上看,属于懒汉式。

2.3 双重检查

双重校验(Double Checked Locking, DCL),使用volatile和双重检查实现单例和线程安全。

public class SingletonObject {
	//注意,volatile
    private volatile static SingletonObject instance;  
    
    private SingletonObject (){}  
    
    public static SingletonObject getInstance() {  
    	if (instance== null) {  
        	synchronized (SingletonObject.class) {  
        		if (instance== null) {  
            		instance= new SingletonObject();  
        		}  
        	}  
    	}  
    	return instance;  
    }  
 }

此类实现从分类上看,属于懒汉式。

注意,volatile在此不可省略,省略volatile可能在一些机器上行得通,但当有编译器进行重排序的时候,多个线程里可能会存在某个线程获取到实例的中间态而非实例本身。

用锁保证线程安全的方式不只有双重检查这一种,也可以将synchronized 应用在方法上,

public class SingletonObject {
    private static SingletonObject INSTANCE;

    private SingletonObject () {}

    public static synchronized SingletonObject getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SingletonObject ();
        }
        return INSTANCE;
    }
}

这种方法也可以实现线程同步,保证线程安全,但是效率会大打折扣。同时,若有意无意的缺省掉方法上的synchronized 关键字时,甚至无法运行在多线程环境下,否则会出现多个实例。

3 总结

看到有一些资料上写了使用枚举的方式,因为还未接触且使用到,所以本文权且不将其纳入范围,待接触有一定了解之后再行添加。

单例模式用于创建单一实例,虽然简单且实现该模式的方法也有很多种,但线程安全问题不可忽略。

4 参考文献

【1】https://www.runoob.com/design-pattern/singleton-pattern.html
【2】https://www.cnblogs.com/xz816111/p/8470048.html
【3】 https://baike.baidu.com/item/%E9%87%8C%E6%B0%8F%E4%BB%A3%E6%8D%A2%E5%8E%9F%E5%88%99

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值