Java 设计模式系列(一)单例模式

时间:2022-08-24 09:50:48

设计模式之美 - 单例模式

设计模式之美目录:https://www.cnblogs.com/binarylei/p/8999236.html

保证一个类只有一个实例,并且提供一个访可该实例的全局访问点。实现方式有三种:懒汉式、饿汉式、注册式(包括枚举)。应用场景如:Listener 本身单例、日历 Calender、IOC 容器、配置信息 Config。

在学习单例模式的过程中,思考如下两个问题:

  1. 为什么说饿汉式(提前初始化)优于懒汉式?
  2. 为什么不推荐使用单例模式?有何替代方案?

1. 单例实现方式

1.1 饿汉式单例

// 饿汉式单例类:在类初始化时,已经自行实例化
public class Singleton {
private Singleton() {}
private static final Singleton single = new Singleton();
// 静态工厂方法
public static Singleton getInstance() {
return single;
}
}

说明: 饿汉式在类创建时就已经创建好静态对象,以后不再改变,所以天生是线程安全的。有人觉得饿汉式单例不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,我个人并不认同这样的观点。

  • 占用资源多:按照 fail-fast 设计原则,如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM),避免在程序运行时,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。
  • 初始化耗时长:避免程序运行时,突然初始化该类,导致响应时间变长。

汉式和懒汉式区别:

  1. 线程安全。饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,懒汉式本身是非线程安全的,常用的保证单例线程安全的方案有 "double-check" 和 "静态内部类" 两种。

  2. 资源加载和性能。

1.2 懒汉式单例

/**
* 懒汉式单例类:在第一次调用的时候实例化自己
* 1. 构造器私有化,避免外面直接创建对象
* 2. 声明一个私有的静态变量
* 3. 创建一个对外的公共静态方法访问该变量,如果没有变量就创建对象
*/
public class Singleton {
private Singleton() {
} private static Singleton single = null;
// 静态工厂方法
public static Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}

说明: Singleton 通过将构造方法限定为 private 避免了类在外部被实例化,在同一个虚拟机范围内,Singleton 的唯一实例只能通过 getInstance() 方法访问。事实上,通过 Java 反射机制是能够实例化构造方法为 private 的类的,那基本上会使所有的 Java 单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。

但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个 Singleton 实例,要实现线程安全,最简单粗暴的方法是在 getInstance 方法上加同步锁。

public class Singleton {
private Singleton() {}
private static Singleton single=null;
// 静态工厂方法
public static synchronized Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}

1.3 双重检查锁定

为处理同步延迟加载方式瓶颈问题,我们需要对 instance 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同获取锁了),但在 Java 中行不通,因为同步块外面的 if (instance == null) 可能看到已存在,但不完整的实例。JDK5.0 以后版本若 instance 为 volatile 则可行:

public class Singleton {
private volatile static Singleton instance = null;
private Singleton() {
} public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {// 1
if (instance == null) { // 2
instance = new Singleton();// 3
}
}
}
return instance;
}
}

可以看到里面加了 volatile 关键字来声明单例对象,既然 synchronized 已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加 volatile 呢,原因已经在下面评论中提到:

《单例模式与双重检测》:http://www.iteye.com/topic/652440

1.4 静态内部类

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

使用 Java 初始化类时的线程安全机,保障线程安全,同时避免了同步带来的性能影响。

1.5 枚举

枚举的本质是懒汉式单例,在类初始化时会实例对象。我们看一下枚举类反编译后的代码就一目了然:

本文使用 jad 工具进行反编译,下载地址:https://varaneckas.com/jad/

public final class SingletonEnum extends Enum {
private SingletonEnum(String s, int i) {
super(s, i);
} public static final SingletonEnum A;
public static final SingletonEnum B;
public static final SingletonEnum C;
private static final SingletonEnum $VALUES[]; static {
A = new SingletonEnum("A", 0);
B = new SingletonEnum("B", 1);
C = new SingletonEnum("C", 2);
$VALUES = (new SingletonEnum[] { A, B, C });
}
}

1.6 注册式单例

Spring 内部使用注册式实现单例,枚举也是注册式。

// 类似 Spring 里面的方法,将类名注册,下次从里面直接获取。
public class Singleton {
private static Map<String, Singleton> map = new HashMap<String, Singleton>();
// 静态工厂方法,返还此类惟一的实例
public static Singleton getInstance(String name) {
if(map.get(name) == null) {
map.put(name, (Singleton) Class.forName(name).getInstance());
}
return map.get(name);
}
}

说明: 登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个 Map 中,对于已经登记过的实例,则从 Map 直接返回,对于没有登记的,则先登记,然后返回。

2. 单例作用范围

2.1 进程内单例

Java 进行内单例为什么说都是 ClassLoader 级别的呢?

我理解单例的本质还是使用缓存将对象保存起来,单例的作用范围实际上看能不能缓存中获取这个对象。如线程内的单例,使用 ThreadLocal 进行缓存时,对其它线程不可见。而进程内单例,也就是普通的单例,无论是枚举或懒汉或贪婪模式都使用 static 进行缓存,而不同 ClassLoader 加载的静态空间实际上是不共享的,所以进程间的单例实际上是 ClassLoader 级别的。如果不同 ClassLoader 间的 static 静态空间共享,那么像 Tomcat 容器,每个应用都会初始化一个 ClassLoader 加载应用,不同应用之间就可能会相互修改其它应用的数据,这样会出很多问题。

2.2 线程内单例

使用 JDK 提供了 ThreadLocal 来保证线程内唯一。

2.3 集群内单例

  1. 单例对象序列化并存储到外部共享存储区(比如文件)。
  2. 进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。
  3. 为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。

3. 什么时候使用单例

其实,我一般不推荐使用单例模式。使用单例模式会带来很多问题。

  • 单例对 OOP 特性的支持不友好。单例的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的 OOP 的抽象特性。在代码中直接使用 Singleton#getInstance() 获取实例对象,实际上是基于实现编程而非接口编程,也就是我们所说的硬编码。如果后续需要扩展其余的实现,那么项目中所以使用单例硬编码的地方都要修改。
  • 单例会隐藏类之间的依赖关系。一般只能通过 Singleton#getInstance() 获取对象,隐藏依赖的对象。
  • 单例对代码的扩展性不友好。比如数据库连接池如果设计为单例,刚开始我们应用可能只需要一个数据库连接池就可以了,随着应用变得复杂,此时我们需要连接好几个不同的数据库,此时一个单例就无法应对多个实例的情况。
  • 单例对代码的可测试性不友好。单例的所有成员属性都相当于静态变量,这样每次单元测试时都需要手动重置单例,否则不同的单元测试会相互干扰。

既然单例存在这些问题,但项目又要使用单例,那怎么办呢?我总结了以下几种常用的方法:

  1. 程序员自己控制单例类的使用。只要不是提供给第三方使用,自己完全可以控制类的行为,没有必要专门设计成单例。

  2. 依赖注入:如 Spring IoC 默认就是单例。

  3. 工厂方法:工厂方法返回单例对象,这种方法其实也很常用。

    public class RuleConfigParserFactory_v2 {
    private static final Map<String, IRuleConfigParser> cachedParsers = new HashMap<>();
    static {
    cachedParsers.put("json", new JsonRuleConfigParser());
    cachedParsers.put("xml", new XmlRuleConfigParser());
    cachedParsers.put("yaml", new YamlRuleConfigParser());
    cachedParsers.put("properties", new PropertiesRuleConfigParser());
    } public static IRuleConfigParser createParser(String configFormat) {
    return cachedParsers.get(configFormat.toLowerCase());
    }
    }

每天用心记录一点点。内容也许不重要,但习惯很重要!

Java 设计模式系列(一)单例模式的更多相关文章

  1. Java设计模式系列之单例模式

    单例模式的定义 一个类有且仅有一个实例,并且自行实例化向整个系统提供.比如,多程序读取一个配置文件时,建议配置文件时,建议配置文件封装成对象.会方便操作其中的数据,又要保证多个程序读到的是同一个配置文 ...

  2. Java设计模式之《单例模式》及应用场景

    摘要: 原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/6510196.html 所谓单例,指的就是单实例,有且仅有一个类实例,这个单例不应该 ...

  3. Java设计模式系列-抽象工厂模式

    原创文章,转载请标注出处:https://www.cnblogs.com/V1haoge/p/10755412.html 一.概述 抽象工厂模式是对工厂方法模式的再升级,但是二者面对的场景稍显差别. ...

  4. Java设计模式系列-工厂方法模式

    原创文章,转载请标注出处:<Java设计模式系列-工厂方法模式> 一.概述 工厂,就是生产产品的地方. 在Java设计模式中使用工厂的概念,那就是生成对象的地方了. 本来直接就能创建的对象 ...

  5. Java设计模式系列-装饰器模式

    原创文章,转载请标注出处:<Java设计模式系列-装饰器模式> 一.概述 装饰器模式作用是针对目标方法进行增强,提供新的功能或者额外的功能. 不同于适配器模式和桥接模式,装饰器模式涉及的是 ...

  6. Java设计模式之【单例模式】

    Java设计模式之[单例模式] 何为单例 在应用的生存周期中,一个类的实例有且仅有一个 当在一些业务中需要规定某个类的实例有且仅有一个时,就可以用单例模式 比如spring容器默认初始化的实例就是单例 ...

  7. Java设计模式中的单例模式

    有时候在实际项目的开发中,我们会碰到这样一种情况,该类只允许存在一个实例化的对象,不允许存在一个以上的实例化对象,我们将这种情况称为Java设计模式中的单例模式.设计单例模式主要采用了Java的pri ...

  8. Java 设计模式系列(二三)访问者模式&lpar;Vistor&rpar;

    Java 设计模式系列(二三)访问者模式(Vistor) 访问者模式是对象的行为模式.访问者模式的目的是封装一些施加于某种数据结构元素之上的操作.一旦这些操作需要修改的话,接受这个操作的数据结构则可以 ...

  9. Java 设计模式系列(十五)观察者模式&lpar;Observer&rpar;

    Java 设计模式系列(十五)观察者模式(Observer) Java 设计模式系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Java ...

  10. Java 设计模式系列(十八)备忘录模式&lpar;Memento&rpar;

    Java 设计模式系列(十八)备忘录模式(Memento) 备忘录模式又叫做快照模式(Snapshot Pattern)或Token模式,是对象的行为模式.备忘录对象是一个用来存储另外一个对象内部状态 ...

随机推荐

  1. Chrome 开发工具之Network

    经常会听到比如"为什么我的js代码没执行啊?","我明明发送了请求,为什么反应?","我这个网站怎么加载的这么慢?"这类的问题,那么问题既然 ...

  2. Entity Framework 6&comma; database-first with Oracle

    Entity Framework 6, database-first with Oracle 转载自http://csharp.today/entity-framework-6-database-fi ...

  3. 应用服务器和Web服务器

    如上图所示,绝大部分的公司会采用Apache+tomcat集群(或jetty集群)来部署公司的Web服务, Web服务器和应用服务器关系,先介绍一下我们常说的服务器: Tomcat服务器,是运行ser ...

  4. php目录下的ext目录中,执行的命令

    php的目录下的ext目录,如果你只需要一个基本的扩展框架的话,执行下面的命令: ./ext_skel --extname=module_name module_name是你自己可以选择的扩展模块的名 ...

  5. 8&period;python中的数字

    python中数字对象的创建如下, a = 123 b = 1.23 c = 1+1j 可以直接输入数字,然后赋值给变量. 同样也可是使用类的方式: a = int(123) b = float(1. ...

  6. &lbrack;HDOJ4738&rsqb;Caocao&&num;39&semi;s Bridges(双联通分量,割边,tarjan)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4738 给一张无向图,每一条边都有权值.找一条割边,使得删掉这条边双连通分量数量增加,求权值最小那条. ...

  7. w3school之CSS学习笔记

    由于web自动化测试中,会用到比较复杂的定位方式:CSS定位,这种定位方式比较简洁,定位速度较快,所以继续学习前端的CSS知识,总结下学习笔记,以便后续查看.同时,也希望能帮助到大家. 学习网址:ht ...

  8. Maven安装和使用

    一.安   装 1.解压好后,添加系统环境变量 变量名:MAVEN_HOME 属性值:D:\apache-maven-3.3.3  //也就是解压的路径 path中添加:%MAVEN_HOME%\bi ...

  9. 如何方便的在windows测试python程序

    听说python的网页抓取模块很强大,我想试试看看能给我的网络优化工作带来什么大的帮助,于是跟随廖雪峰老师开始学习python(地址查看),因为我用的是window系统,这就给程序的测试带来了很多麻烦 ...

  10. 密码&bsol;路径&bsol;IP&period;&period;&period;备忘录

    1.linux 192.168.200.128:22 root/123456