【设计模式】Singleton

本文深入探讨了Singleton设计模式的实现方式及线程安全性问题,并通过代码示例验证了其在多线程环境下的表现。此外,文章还分析了使用Singleton模式可能带来的弊端,包括全局变量的问题和对单一职责原则的影响。

前言

Singleton设计模式,确保全局只存在一个该类的实例。将构造器声明为private,防止调用(虽然还是可以使用反射来调用)。声明一个静态的类实例在类中,声明一个公共的获取实例的方法。这篇博文给出了简单的实现方法,分析如何做到线程安全,整理了使用Singleton的坏处。

1616773-20190723182612309-558402922.png

线程安全

方法一是线程安全的,在类被装载的时候,就初始化这个成员,Java库中Runtime就是用了这个方法。
方法二不是线程安全的。如果多个线程同时进入到函数中(临界区),那么会返回多个不同的实例。

代码验证

以下代码验证了线程不安全,即多线程的情况下方法二不能保证真正的“单例”。通过打印每个类的hashCode来显示,类实例不唯一。

class Singleton {
    private static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            try {
                Thread.sleep(233);
                singleton = new Singleton();
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
        return singleton;
    }
}

public class Main {
    public static void main(String args[]) {
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                public void run() {
                    Singleton singleton = Singleton.getInstance();
                    System.out.println(singleton.hashCode());
                }
            }).start();
        }
    }
}

解决方案

一种简单的处理方法是,将getInstance声明为synchronized的,效率低,因为每一个线程都在等待进入临界区。

另一种方法叫做Double Checked Locking(DCL),将公共变量声明为volatile(每次要使用这个变量的时候,从内存中取出来,保证各个线程可以看到这个变量的修改)。

class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            try {
                Thread.sleep(233);
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
        return singleton;
    }
}

坏处

有啥坏处,别怪这个设计模式,都是全局变量的错。

1, 全局变量

Singleton管理公共的资源,在代码中的任何地方,只要调用Singleton的获取实例的方法,就可以获取到Singleton的实例,可以对这些公共资源的读写。这让Singleton成为了一个全局变量。

全局变量的坏处,也是Singleton的坏处。全局变量自有全局变量的使用情景和优点,要分开两面看待,这里只讲坏处并非想说全局变量一无是处。

在分析全局变量的坏处之前,在[2]下面看到了一个特别有意思的比喻:

Using global state with your fellow programmers is like using the same toothbrush with your friends - you can but you never know when someone will decide to shove it up his bum.

我想这位老兄说出这个观点,大概是用过异味的牙刷吧。为何不用规范来约束一同使用全局变量的人呢?

这位老兄道出了全局变量的本质,任何人可以用它做任何事,给整个程序带来了极大的不确定性。网上关于全局变量的坏处讨论很多,下面整理了一些,虽然并非都很坏:

  1. 代码耦合程度更高。假设一些函数依赖于全局变量的状态,这些函数通过这个全局变量联系到一起。一个函数的修改对另一个函数的读取存在影响,这些函数在无形中联系到了一起。
  2. 给测试带来困难。全局变量存储了一些状态,需要安排好模块的运行顺序才可以正确的测试。可是,单元测试应该是相互独立的,而非有顺序的。
  3. 多线程的写入的时候,要互斥。
  4. 破坏了函数的输入输出功能。拿全局变量来读输入,写输出。
  5. 命名冲突。
  6. 可读性降低。要理解一个函数,还需要去跟踪使用到的全局变量的来龙去脉。

2, 破坏了单一职责原则

定义:就一个类而言,应该仅有一个引起它变化的原因

当一个类使用了Singleton的时候,它不仅负责这个类需要完成的任务,还负责这个单一对象资源的创建和管理。这个类的函数做两项任务,相关性低。

这一点算不上太坏。毕竟设计原则并不总是需要遵守的。

正确使用

上面讲了坏处,核心要义是避免将Singleton用作全局变量。Singleton的使用场景是什么呢?

回到Singleton的本质作用上来,只需要一个这个类的实例。前面讲这个设计模式破坏了单一职责原则,因为需要管理这个实例。

所以归结起来使用这个设计模式的两个原因:

(1) 单个实例

(2) 实例的管理

在[1]中讲了使用Singleton的一个具体例子。那就是日志(log)。因为log和代码的实质功能并不会产生耦合,所以是否开启log对于系统的功能没有太大的影响。

参考链接

  1. https://stackoverflow.com/questions/228164/on-design-patterns-when-should-i-use-the-singleton
  2. https://softwareengineering.stackexchange.com/questions/148108/why-is-global-state-so-evil
  3. https://stackoverflow.com/questions/26285520/implementing-singleton-with-an-enum-in-java
  4. https://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons?page=1&tab=votes#tab-top

转载于:https://www.cnblogs.com/zhouzekai/p/11233617.html

### 单例设计模式Singleton Pattern)的概念 单例模式是一种常用的软件设计模式,其核心思想是确保一个类只有一个实例存在,并提供一个全局访问点来访问该实例。这种模式在系统中需要协调全局行为时非常有用,例如管理配置信息、日志记录、线程池等场景。通过单例模式,可以避免多个实例造成的资源浪费或状态不一致问题[^1]。 ### 单例设计模式的用途 单例模式的主要用途包括: 1. **统一资源管理**:例如服务器的配置信息通常由一个单例对象读取,其他对象通过该对象获取配置信息,简化了复杂环境下的配置管理[^1]。 2. **全局访问点**:确保某个类的实例在整个应用程序中只有一个,并且可以被全局访问。 3. **资源共享**:如数据库连接池、线程池等,避免重复创建和销毁资源带来的性能开销。 ### 单例设计模式的实现方式 单例模式的实现通常需要满足以下几个关键点: 1. **私有化构造函数**:防止外部直接通过 `new` 关键字创建实例。 2. **静态变量保存实例**:在类内部定义一个静态变量用于保存类的唯一实例。 3. **提供全局访问方法**:通过静态方法返回类的实例。 #### 1. 饿汉式(Eager Initialization) 饿汉式在类加载时就创建实例,因此线程安全。适用于实例创建成本不高且需要立即加载的场景。 ```java public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } } ``` 这种方式的优点是简单且线程安全,但缺点是无论是否使用该实例,都会在类加载时创建,可能造成资源浪费[^2]。 #### 2. 懒汉式(Lazy Initialization) 懒汉式在第一次调用 `getInstance()` 时才创建实例,适用于实例创建成本较高的场景。 ```java public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } ``` 这种方式虽然节省了资源,但在多线程环境下可能会导致多个实例被创建,因此需要额外的同步机制来保证线程安全[^2]。 #### 3. 双重校验锁(Double-Checked Locking) 双重校验锁是一种优化的懒汉式实现,通过 `synchronized` 关键字和 `volatile` 关键字确保线程安全并减少同步开销。 ```java public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } ``` 这种方式在多线程环境下能够高效地保证单例的唯一性,同时避免了不必要的同步开销[^2]。 #### 4. 静态内部类(Static Nested Class) 静态内部类利用了类加载机制来保证线程安全,同时实现了懒加载。 ```java public class Singleton { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } } ``` 这种方式在调用 `getInstance()` 时才会加载内部类并创建实例,因此既保证了线程安全,又避免了不必要的资源浪费[^2]。 #### 5. 枚举(Enum) 枚举是实现单例模式的一种简洁且线程安全的方式,尤其适合需要序列化或反序列化的场景。 ```java public enum Singleton { INSTANCE; public void doSomething() { // 业务逻辑 } } ``` 枚举实现的单例模式天然支持序列化和反序列化,并且可以避免反射攻击,是一种推荐的实现方式。 #### 6. JavaScript 中的实现 在 JavaScript 中,可以通过闭包或类来实现单例模式。 **闭包实现**: ```javascript let Singleton = function (name) { this.name = name; }; Singleton.getInstance = (function () { let instance = null; return function (name) { if (!instance) { instance = new Singleton(name); } return instance; }; })(); ``` **类实现(ES6)**: ```javascript class Singleton { constructor(name) { if (!Singleton.instance) { this.name = name; Singleton.instance = this; } return Singleton.instance; } } const s1 = new Singleton('A'); const s2 = new Singleton('B'); console.log(s1 === s2); // true ``` 这些实现方式在 JavaScript 中能够确保全局只有一个实例,并且可以通过闭包或类的方式实现懒加载和线程安全[^3]。 ### 单例模式的优缺点 **优点**: - 提供了对唯一实例的受控访问。 - 节省系统资源,避免重复创建和销毁实例。 - 可以全局共享状态。 **缺点**: - 违反了单一职责原则,单例类可能承担过多职责。 - 难以进行单元测试,因为单例的全局状态可能导致测试之间的依赖。 - 在分布式系统中,单例模式可能难以扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值