设计模式 之 单例模式

项目源码:https://gitee.com/Jacob-gitee/DesignMode

个人博客:https://jacob.org.cn

宗旨

    Ensure a class has only one instance,and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

一个皇帝原则

    皇帝每天要上朝接待臣子、处理政务,臣子每天要叩拜皇帝,皇帝只能有一个,也就是一个类只能产生一个对象,该怎么实现呢?对象产生是通过new关键字完成的(当然也有其他方式,比如对象拷贝、反射等),这个怎么控制呀,但是大家别忘记了构造函数,使用new关键字创建对象时,都会根据输入的参数调用相应的构造函数,如果我们把构造函数设置为private私有访问权限不就可以禁止外部创建对象了吗?臣子叩拜唯一皇帝的过程如类图7-1所示。

image-20200814173049558

    图7-1 臣子叩拜皇帝类图

    只有两个类,Emperor代表皇帝类,Minister代表臣子类,关联到皇帝类非常简单。Emperor如代码清单7-1所示。

    代码清单7-1 皇帝类

public class Emperor {private static final Emperor emperor = new Emperor();//初始化一个皇帝public Emperor() {//世俗和道德约束你,目的就是不希望产生第二个皇帝}public static Emperor getInstance(){return emperor;}//皇帝发话了public void say(){System.out.println("我就是皇帝Jacob...");}}

    通过定义一个私有访问权限的构造函数,避免被其他类new出来一个对象,而Emperor自己则可以new一个对象出来,其他类对该类的访问都可以通过getInstance获得同一个对象。
    皇帝有了,臣子要出场,其类如代码清单7-2所示。

    代码清单7-2 臣子类

//代码清单7-2 臣子类
public class Minister {public static void main(String[] args) {for (int day=0;day<3;day++){Emperor emperor = Emperor.getInstance();//打印一下地址,判断是否相同System.out.println(emperor);emperor.say();}}
}臣子参拜皇帝的运行结果如下所示。
我就是皇帝Jacob...
cn.wzq.design.mode.singleton.Emperor@299a06ac
我就是皇帝Jacob...
cn.wzq.design.mode.singleton.Emperor@299a06ac
我就是皇帝Jacob...

    臣子天天要上朝参见皇帝,今天参拜的皇帝应该和昨天、前天的一样(过渡期的不考虑,别找茬哦),大臣磕完头,抬头一看,嗨,还是昨天那个皇帝,老熟人了,容易讲话,这就是单例模式。

单例模式的定义

    单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:
    Ensure a class has only one instance,and provide a global point of access to it.(确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。)

    单例模式的通用类图如图7-2所示

image-20200814173207109

    如图7-2 单例模式通用类

    Singleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实例,并且是自行实例化的(在Singleton中自己使用new Singleton())。

    单例模式的通用源代码如代码清单7-3所示。

    代码清单7-3 单例模式通用代码

public class Emperor {private static final Emperor emperor = new Emperor();//初始化一个皇帝//限制产生多个对象public Emperor() {}//通过该方法获取实例对象public static Emperor getInstance(){return emperor;}public static void doSomething(){System.out.println("皇帝要睡觉了...");}
}

单例模式的应用

单例模式的优点
  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。

  • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。

  • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

单例模式的缺点
  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。

  • 单例模式对测试是不利的。在并行开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也不能使用mock的方式虚拟一个对象。

  • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要单例取决于环境,单例模式把“要单例”和业务逻辑融合在一个类中。

单例模式的使用场景

    在一个系统中,要求一个类有且仅有一个对象,如果出现多个对象就会出现“不良反应”,可以采用单例模式,具体的场景如下:

  • 要求生成唯一序列号的环境;
  • 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保是线程安全的;
  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(当然,也可以直接声明为static的方式)。
单例模式的注意事项

    首先,在高并发情况下,请注意单例模式的线程同步问题。单例模式有几种不同的实现方式,上面的例子不会出现产生多个实例的情况,但是如代码清单7-4所示的单例模式就需要考虑线程同步。

    代码清单7-4 线程不安全的单例

public class Singleton {private static Singleton singleton = null;public Singleton() {}public static Singleton getInstance(){if (singleton == null){singleton = new Singleton();}return singleton;}
}

    该单例模式在低并发的情况下尚不会出现问题,若系统压力增大,并发量增加时则可能在内存中出现多个实例,破坏了最初的预期。为什么会出现这种情况呢?如一个线程A执行到singleton=new Singleton(),但还没有获得对象(对象初始化是需要时间的),第二个线程B也在执行,执行到(singleton==null)判断,那么线程B获得判断条件也是为真,于是继续运行下去,线程A获得了一个对象,线程B也获得了一个对象,在内存中就出现两个对象!

    解决线程不安全的方法有很多,可以在getSingleton方法前加synchronized关键字,也可以在getSingleton方法内增加synchronized来实现,但都不是最优秀的单例模式,建议读者使用如代码清单7-3所示的方式(有的书上把代码清单7-3中的单例称为饿汉式单例,在代码清单7-4中增加了synchronized的单例称为懒汉式单例)。

    其次,需要考虑对象的复制情况。在Java中,对象默认是不可以被复制的,若实现了Cloneable接口,并实现了clone方法,则可以直接通过对象复制方式创建一个新对象,对象复制是不用调用类的构造函数,因此即使是私有的构造函数,对象仍然可以被复制。在一般情况下,类复制的情况不需要考虑,很少会出现一个单例类会主动要求被复制的情况,解决该问题的最好方法就是单例类不要实现Cloneable接口。

    如果一个类可以产生多个对象,对象的数量不受限制,则是非常容易实现的,直接使用new关键字就可以了,如果只要有一个对象,使用单例模式就可以了,但是如果要求一个类只能产生两三个对象呢?该怎么实现?我们还以皇帝为例来说明。

    一般情况下,一个朝代的同一个时代只有一个皇帝,那有没有出现两个皇帝的情况呢?确实有,就出现在明朝,那三国期间的算不算?不算,各自称帝,各有各的地盘,国号不同。大家还记得《石灰吟》这首诗吗?作者是谁?于谦。他是被谁杀死的?明英宗朱祁镇。对,就是那个在土木堡之变中被瓦刺俘虏的皇帝,被俘虏后,他弟弟朱祁钰当上了皇帝,就是明景帝,估计刚当上皇帝乐疯了,忘记把他哥哥朱祁镇升级为太上皇,在那个时期就出现了两个皇帝,这期间的的大臣是非常郁闷的,为什么呀?因为可能出现今天参拜的皇帝和昨天的皇帝不相同,昨天给那个皇帝汇报,今天还要给这个皇帝汇报一遍,该情况的类图如图7-3所示。

image-20200814173317563

    图7-3 多个皇帝类

    这个类图看起来还算简单,但是实现就有点复杂了。Emperor类如代码清单7-5所示。

//代码清单7-5 固定数量的皇帝类
public class Emperor2 {//定义最多能产生的实例数量private static int maxNumOfEmperor = 2;//每个皇帝都有名字,使用一个ArrayList来容纳,每个对象的私有属性private static ArrayList<String> nameList = new ArrayList<String>();//定义一个列表,容纳所有的皇帝实例private static ArrayList<Emperor2> emperorList = new ArrayList<Emperor2>();//当前皇帝序列号private static int countNumOfEmperor = 0;//产生所有的对象static {for (int i=0;i<maxNumOfEmperor;i++){emperorList.add(new Emperor2("皇帝"+i));}}//皇帝名称private String name;public Emperor2() {}public Emperor2(String name) {nameList.add(name);}public static Emperor2 getInstance(){Random random = new Random();countNumOfEmperor = random.nextInt(maxNumOfEmperor);return emperorList.get(countNumOfEmperor);}public  void say(){System.out.println(nameList.get(countNumOfEmperor));}
}

    在Emperor中使用了两个ArrayList分别存储实例和实例变量。当然,如果考虑到线程安全问题可以使用Vector来代替。臣子参拜皇帝的过程如代码清单7-6所示。

    代码清单7-6 臣子参拜皇帝的过程

public class Minister2 {public static void main(String[] args) {//定义5个大臣int ministerNum=5;for(int i=0;i<ministerNum;i++) {Emperor2 emperor = Emperor2.getInstance();System.out.print("第" + (i + 1) + "个大臣参拜的是:"+emperor);emperor.say();}}
}大臣参拜皇帝的结果如下所示。
第1个大臣参拜的是:cn.wzq.design.mode.singleton.Emperor2@383534aa皇帝12个大臣参拜的是:cn.wzq.design.mode.singleton.Emperor2@383534aa皇帝13个大臣参拜的是:cn.wzq.design.mode.singleton.Emperor2@6bc168e5皇帝04个大臣参拜的是:cn.wzq.design.mode.singleton.Emperor2@6bc168e5皇帝05个大臣参拜的是:cn.wzq.design.mode.singleton.Emperor2@383534aa皇帝1

    看,果然每个大臣参拜的皇帝都可能不一样,大臣们就开始糊涂了,A大臣给皇1帝汇报了一件事情,皇2帝不知道,然后就开始怀疑大臣A是皇1帝的亲信,然后就想办法开始整……
这种需要产生固定数量对象的模式就叫做有上限的多例模式,它是单例模式的一种扩展,采用有上限的多例模式,我们可以在设计时决定在内存中有多少个实例,方便系统进行扩展,修正单例可能存在的性能问题,提供系统的响应速度。例如读取文件,我们可以在系统启动时完成初始化工作,在内存中启动固定数量的reader实例,然后在需要读取文件时就可以快速响应。

最佳实践

    单例模式是23个模式中比较简单的模式,应用也非常广泛,如在Spring中,每个Bean默认就是单例的,这样做的优点是Spring容器可以管理这些Bean的生命期,决定什么时候创建出来,什么时候销毁,销毁的时候要如何处理,等等。如果采用非单例模式(Prototype类型),则Bean初始化后的管理交由J2EE容器,Spring容器不再跟踪管理Bean的生命周期。

    使用单例模式需要注意的一点就是JVM的垃圾回收机制,如果我们的一个单例对象在内存中长久不使用,JVM就认为这个对象是一个垃圾,在CPU资源空闲的情况下该对象会被清理掉,下次再调用时就需要重新产生一个对象。如果我们在应用中使用单例类作为有状态值(如计数器)的管理,则会出现恢复原状的情况,应用就会出现故障。如果确实需要采用单例模式来记录有状态的值,有两种办法可以解决该问题:

  • 由容器管理单例的生命周期

    Java EE容器或者框架级容器(如Spring)可以让对象长久驻留内存。当然,自行通过管理对象的生命期也是一个可行的办法,既然有那么多的工具提供给我们,为什么不用呢?

  • 状态随时记录

    可以使用异步记录的方式,或者使用观察者模式,记录状态的变化,写入文件或写入数据库中,确保即使单例对象重新初始化也可以从资源环境获得销毁前的数据,避免应用数据丢失。

学习于:《设计模式之禅》 — 秦小波

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/273567.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计模式 之 抽象工厂模式

项目源码&#xff1a;https://gitee.com/Jacob-gitee/DesignMode 个人博客 &#xff1a;https://jacob.org.cn 女娲的失误 工厂模式中讲了女娲造人的故事。人是造出来了&#xff0c;世界也热闹了&#xff0c;可是低头一看&#xff0c;都是清一色的类型&#xff0c;缺少关爱、仇…

设计模式 之 模板模式

项目源码&#xff1a;https://gitee.com/Jacob-gitee/DesignMode 个人博客 &#xff1a;http://jacob.org.cn 女娲的失误 工厂模式中讲了女娲造人的故事。人是造出来了&#xff0c;世界也热闹了&#xff0c;可是低头一看&#xff0c;都是清一色的类型&#xff0c;缺少关爱、仇…

使用Java高速实现进度条

基于有人问到如何做进度条&#xff0c;以下给个简单的做法&#xff1a; 主要是使用JProgressBar&#xff08;Swing内置javax.swing.JProgressBar&#xff09;和SwingWorker&#xff08;Swing内置javax.swing.SwingWorker&#xff09; 有人肯定会说&#xff0c;不是用线程做的吗…

关于js的function.来自百度知道的回答,学习了.

在js中&#xff0c;创建一个函数对象的语法是var myFunction new Function(arg1,…,agrN, body);其中&#xff0c;该函数对象的N个参数放在 函数主体参数body的前面&#xff0c;即函数主体参数必须放在参数列表的最后&#xff0c;也可以无参数new Function(body)。你添加第三个…

fedora20开机启动配置:systemctl

老版fedora中使用chkconfig配置开机启动&#xff0c;fedora20中&#xff0c;使用chkconfig会出现各种问题。使用systemctl配置。 具体表格如下 转载于:https://www.cnblogs.com/hh6plus/p/5548083.html

《Java 高并发》01 高并发基本概念

基本概念 同步和异步 同步和异步通常是用来形容一次方法调用。 同步方法调用一旦开始&#xff0c;调用者必须等到方法返回才能继续执行后续操作。 异步方法调用更像一个消息传递&#xff0c;一旦开始&#xff0c;方法调用就会立即返回&#xff0c;调用者就可以继续后续的操…

Android之Http网络编程(四)

前面几篇博文简单的介绍了一些常见的Http的操作&#xff0c;这些操作几乎都是在新开的线程中进行的网络请求&#xff0c;并在日志中打印出获取到的网络数据。那么&#xff0c;问题来了&#xff01;&#xff08;呃~感觉下一句是蓝翔有木有&#xff1f;&#xff09;如何在把获取到…

mybatis 2 -常用数据操作

1、写入数据并获取自增ID XML配置&#xff1a; <!-- 写入数据获取自增ID --><insert id"insertLog" parameterType"com.mamaguwen.entity.sys_loginlog" useGeneratedKeys"true" keyProperty"logid">insert into sys_…

Docker 搭建 ELK 日志系统,并通过 Kibana 查看日志

Docker 搭建 ELK 日志系统,并通过 Kibana 查看日志 docker-compose.yml version: 3 services:elasticsearch:image: elasticsearch:7.7.0 #镜像container_name: elasticsearch #定义容器名称restart: always #开机启动&#xff0c;失败也会一直重启environment:- "cl…

RabbitMQ Management:Management API returned status code 500

错误显示&#xff1a; 解决方案&#xff1a; 因为是使用docker 容器安装的&#xff0c;所有需要进入容器 docker exec -it rabbitmq /bin/bash进入目录 cd /etc/rabbitmq/conf.d/执行命令 echo management_agent.disable_metrics_collector false > management_agent.dis…

c++ 读取文件 最后一行读取了两次

用ifstream的eof()&#xff0c;竟然读到文件最后了&#xff0c;判断eof还为false。网上查找资料后&#xff0c;终于解决这个问题。 参照文件&#xff1a;http://tuhao.blogbus.com/logs/21306687.html 在使用C/C读文件的时候&#xff0c;一定都使用过eof&#xff08;&#xff0…

java中的io系统详解(转)

Java 流在处理上分为字符流和字节流。字符流处理的单元为 2 个字节的 Unicode 字符&#xff0c;分别操作字符、字符数组或字符串&#xff0c;而字节流处理单元为 1 个字节&#xff0c;操作字节和字节数组。 Java 内用 Unicode 编码存储字符&#xff0c;字符流处理类负责将外部的…

Unity3D Shader入门指南(二)

关于本系列 这是Unity3D Shader入门指南系列的第二篇&#xff0c;本系列面向的对象是新接触Shader开发的Unity3D使用者&#xff0c;因为我本身自己也是Shader初学者&#xff0c;因此可能会存在错误或者疏漏&#xff0c;如果您在Shader开发上有所心得&#xff0c;很欢迎并恳请您…

JVM:如何分析线程堆栈

英文原文&#xff1a;JVM: How to analyze Thread Dump 在这篇文章里我将教会你如何分析JVM的线程堆栈以及如何从堆栈信息中找出问题的根因。在我看来线程堆栈分析技术是Java EE产品支持工程师所必须掌握的一门技术。在线程堆栈中存储的信息&#xff0c;通常远超出你的想象&…

SQLSERVER中如何忽略索引提示

SQLSERVER中如何忽略索引提示 原文:SQLSERVER中如何忽略索引提示SQLSERVER中如何忽略索引提示 当我们想让某条查询语句利用某个索引的时候&#xff0c;我们一般会在查询语句里加索引提示&#xff0c;就像这样 SELECT id,name from TB with (index(IX_xttrace_bal)) where bal…

[016]转--C++拷贝构造函数详解

一. 什么是拷贝构造函数 首先对于普通类型的对象来说&#xff0c;它们之间的复制是很简单的&#xff0c;例如&#xff1a; [c-sharp] view plaincopy int a 100; int b a; 而类对象与普通对象不同&#xff0c;类对象内部结构一般较为复杂&#xff0c;存在各种成员变量。下…

自定义GrildView实现单选功能

首先看实现功能截图&#xff0c;这是一个自定义Dialog,并且里面内容由GrildView 绑定数据源&#xff0c;实现类似单选功能。 首先自定义Dialog&#xff0c;绑定数据源 自定义Dialog弹出框大小方法 最主要实现的就是点击颜色切换的功能&#xff0c;默认GrildView的每一项都是蓝色…

最大权闭合图hdu3996

定义&#xff1a;最大权闭合图&#xff1a;是有向图的一个点集&#xff0c;且该点集的所有出边都指向该集合。即闭合图内任意点的集合也在改闭合图内&#xff0c;给每个点分配一个点权值Pu&#xff0c;最大权闭合图就是使闭合图的点权之和最大。 最小割建边方式&#xff1a;源点…

非监督学习的单层网络分析

这篇博客对应的是Andrew.Ng的那篇文章&#xff1a;An Analysis o f Single-Layer Networks in Unsupervised Feature Learning&#xff0c;文章的主要目的是讨论receptive field size&#xff0c;number of hidden nodes&#xff0c; step-stride以及whitening在对卷积网络模型…

poj3009 Curling 2.0 深搜

PS&#xff1a;以前看到题目这么长就没写下去了。今天做了半天&#xff0c;没做出来。准备看题解&#xff0c;打开了网站都忍住了&#xff0c;最后还是靠自己做出来的。算是一点进步吧。 分析&#xff1a; 题目的意思没明白或者理解有偏差都没办法做题。看样例3和样例4&#xf…