创建型模式
目录
1、单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
1.1 单例模式UML图
1.2 日常生活中看单例模式
- 1、一个班级只有一个班主任。
- 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
- 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
1.3 使用场景
- 1、要求生产唯一序列号。
- 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
1.4 具体例子
1.4.1 背景
- 在企业网站后台系统中,一般会将网站统计单元进行独立设计,比如登录人数的统计、IP数量的计数等。在这类需要完成全局统计的过程中,就会用到单例模式,即整个系统只需要拥有一个计数的全局对象。
- 在网站登录这个高并发场景下,由这个全局对象负责统计当前网站的登录人数、IP等,即节约了网站服务器的资源,又能保证计数的准确性。
1.4.2 网站计数的单例实现
实现单例模式有多种写法,这里我们只列举其中最常用的三种实现方式,且考虑到网站登录高并发场景下,将重点关注多线程环境下的安全问题。
登录线程的实现:我们先创建一个登录线程类,用于登录及登录成功后调用单例对象进行计数。
/**
* 单例模式的应用--登录线程
*
* @author zhuhuix
* @date 2020-06-01
*/
public class Login implements Runnable {
// 登录名称
private String loginName;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
@Override
public void run() {
// TODO
// 登录成功后调用单例对象进行计数
}
}
主程序的实现:编写一个主程序,利用多线程技术模拟10个用户并发登录,完成登录后输出登录人次计数。
/**
* 单例模式--主程序
*
* @author zhuhuix
* @date 2020-06-01
*/
public class App {
public final static int num = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
Login login = new Login();
login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
threads[i] = new Thread(login);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
// TODO
// 调用单例对象输出登录人数统计
}
1.4.2.1 饿汉模式
- 在程序启动之初就进行创建( 不管三七二十一,先创建出来再说)。
- 天生的线程安全。
- 无论程序中是否用到该单例类都会存在。
/**
* 饿汉式单例模式
*
* @author zhuhuix
* @date 2020-06-01
*/
public class SimpleSingleton implements Serializable {
// 单例对象
private static final SimpleSingleton APP_INSTANCE = new SimpleSingleton();
// 计数器
private AtomicLong count = new AtomicLong(0);
// 单例模式必须保证默认构造方法为私有类型
private SimpleSingleton() {
}
public static SimpleSingleton getInstance() {
return APP_INSTANCE;
}
public AtomicLong getCount() {
return count;
}
public void setCount() {
count.addAndGet(1);
}
}
我们将饿汉模式的单例对象加入进登录线程及主程序中进行测试:
/**
* 单例模式的应用--登录线程
*
* @author zhuhuix
* @date 2020-06-01
*/
public class Login implements Runnable {
// 登录名称
private String loginName;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
@Override
public void run() {
// 饿汉式单例
SimpleSingleton simpleSingleton= SimpleSingleton.getInstance();
simpleSingleton.setCount();
System.out.println(getLoginName()+"登录成功:"+simpleSingleton.toString());
}
}
/**
* 单例模式--主程序
*
* @author zhuhuix
* @date 2020-06-01
*/
public class App {
public final static int num = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
Login login = new Login();
login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
threads[i] = new Thread(login);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println("网站共有"+SimpleSingleton.getInstance().getCount()+"个用户登录");
}
}
输出如下:
10个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式是有效的。
1.4.2.2 懒汉模式
- 在初始化时只进行定义。
- 只有在程序中调用了该单例类,才会完成实例化( 没人动我,我才懒得动)。
- 需通过线程同步技术才能保证线程安全。
我们先看下未使用线程同步技术的例子:
/**
* 懒汉式单例模式--未应用线程同步技术
*
* @author zhuhuix
* @date 2020-06-01
*/
public class LazySingleton {
// 单例对象
private static LazySingleton APP_INSTANCE;
// 计数器
private AtomicLong count = new AtomicLong(0);
// 单例模式必须保证默认构造方法为私有类型
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (APP_INSTANCE == null) {
APP_INSTANCE = new LazySingleton();
}
return APP_INSTANCE;
}
public AtomicLong getCount() {
return count;
}
public void setCount() {
count.addAndGet(1);
}
}
/**
* 单例模式的应用--登录线程
*
* @author zhuhuix
* @date 2020-06-01
*/
public class Login implements Runnable {
....
@Override
public void run() {
// 饿汉式单例
LazySingleton lazySingleton =LazySingleton.getInstance();
lazySingleton.setCount();
System.out.println(getLoginName()+"登录成功:"+lazySingleton);
}
}
/**
* 单例模式--主程序-
*
* @author zhuhuix
* @date 2020-06-01
*/
public class App {
public final static int num = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
Login login = new Login();
login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
threads[i] = new Thread(login);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println("网站共有" + LazySingleton.getInstance().getCount() + "个用户登录");
}
}
输出结果:
10个线程并发登录过程中,获取到了四个对象引用地址,该单例模式失效了。
对代码进行分析:
// 未使用线程同步
public static LazySingleton getInstance() {
// 在多个线程并发时,可能会有多个线程同时进入 if 语句,导致产生多个实例
if (APP_INSTANCE == null) {
APP_INSTANCE = new LazySingleton();
}
return APP_INSTANCE;
}
我们使用线程同步技术对懒汉式模式进行改进:
/**
* 懒汉式单例模式
*
* @author zhuhuix
* @date 2020-06-01
*/
public class LazySingleton {
// 单例对象 ,加入volatile关键字进行修饰
private static volatile LazySingleton APP_INSTANCE;
// 计数器
private AtomicLong count = new AtomicLong(0);
// 单例模式必须保证默认构造方法为私有类型
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (APP_INSTANCE == null) {
// 对类进行加锁,并进行双重检查
synchronized (LazySingleton.class) {
if (APP_INSTANCE == null) {
APP_INSTANCE = new LazySingleton();
}
}
}
return APP_INSTANCE;
}
public AtomicLong getCount() {
return count;
}
public void setCount() {
count.addAndGet(1);
}
}
再测试运行:
10个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式有效了。
1.4.2.3 枚举类实现单例模式
《Effective Java》 推荐使用枚举的方式解决单例模式。这种方式解决了最主要的;线程安全、自由串行化、单一实例。
/**
* 利用枚举类实现单例模式
*
* @author zhuhuix
* @date 2020-06-01
*/
public enum EnumSingleton implements Serializable {
// 单例对象
APP_INSTANCE;
// 计数器
private AtomicLong count = new AtomicLong(0);
// 单例模式必须保证默认构造方法为私有类型
private EnumSingleton() {
}
public AtomicLong getCount() {
return count;
}
public void setCount() {
count.addAndGet(1);
}
}
/**
* 单例模式的应用--登录线程
*
* @author zhuhuix
* @date 2020-06-01
*/
public class Login implements Runnable {
...
@Override
public void run() {
EnumSingleton enumSingleton = EnumSingleton.APP_INSTANCE;
enumSingleton.setCount();
System.out.println(getLoginName()+"登录成功:"+enumSingleton.toString());
}
}
/**
* 单例模式--主程序
*
* @author zhuhuix
* @date 2020-06-01
*/
public class App {
public final static int num = 10;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[num];
for (int i = 0; i < num; i++) {
Login login = new Login();
login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
threads[i] = new Thread(login);
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
threads[i].join();
}
System.out.println("网站共有"+EnumSingleton.APP_INSTANCE.getCount()+"个用户登录");
}
}
输出如下:
10个线程并发登录过程中,该单例模式是有效的。
2、单例模式在源码中的应用
2.1 JDK源码中单例模式
java.lang.Runtime使用了单例模式的饿汉式,源码如下:
2.2 Spring源码中单例模式
在 Spring 依赖注入Bean实例默认是单例的,我们由此展开。bean 可以被定义为两种模式:prototype(多例)和 singleton(单例)。
- singleton(单例):只有一个共享的实例存在,所有对这个 bean 的请求都会返回唯一的实例。
- prototype(多例):对这个 bean 的每次请求都会创建一个新的 bean 实例,类似于 new。
实例:配置文件内容如下:
<bean id="singleton" class="java.util.Date" scope="singleton"></bean>
<bean id="prototype" class="java.util.Date" scope="prototype"></bean>
Spring 中加载单例的过程都是在 BeanFactory 的 getBean() 方法中被定义的,其默认的功能在 AbstractBeanFactory 类中实现,主要包含两个功能。
- 从缓存中获取单例 Bean。
- Bean 的实例中获取对象。
getBean() 方法最终会调用 AbstractBeanFactory 的 doGetBean() 方法,源码如下。
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
//对传入的beanName稍作修改,防止有一些非法字段,然后提取Bean的Name
final String beanName = transformedBeanName(name);
Object bean;
//直接从缓存中获取单例工厂中的objectFactory单例
Object sharedInstance = getsingleton(beanName);
if (sharedInstance != null && args == null) {
if (logger.isDebugEnabled()) {
if (isSingletonCurrentlyInCreation(beanName)) {
logger.debug("Returning eagerly cached instance of singleton bean '" +
beanName + "' that is not fully initialized yet - a consequence of a circular reference");
} else {
}
}
//返回对应的实例,从 Bean实例中获取对象
bean = getObjectForBeanInstance(sharedInstance,name,beanName, null);
} else {
...
}
...
}
getBean() 方法不仅处理单例对象的逻辑,还处理原型对象的逻辑。继续看 getSingleton() 方法的代码实现。
getSingleton() 的工作流程:singletonObjects-->earlySingletonObjects-->singletonFactories-->创建单例实例
/**
* 单例对象的缓存
*/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
//首先通过名字查找这个Bean是否存在
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
//查看缓存中是否存在这个Bean
singletonObject = this.earlySingletonObjects.get(beanName);
//如果这个时候的Bean实例还为空并且允许懒加载
if (singletonObjects == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
在上面代码片段中,synchronized(this.singletonObjects) 是关键,但是前提条件 isSingletonCurrentlyInCreation 的返回值也是 true,也就是这个 Bean 正在被创建。因此,第一次调用 doGetBean() 的时候,getSingleton() 基本上都是返回 null,所以会继续执行 doGetBean() 方法中后面的逻辑。
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
// 获取beanDefinition
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);
// Guarantee initialization of beans that the current bean depends on.
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
}
registerDependentBean(dep, beanName);
try {
getBean(dep);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"'" + beanName + "' depends on missing bean '" + dep + "'", ex);
}
}
}
// Create bean instance.
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
}
...
}
}
可以看到,在 BeanFactory 中,从 XML 中解析出来的相关配置信息被放在 BeanDefinitionMap 中,通过这个 Map 获取 RootBeanDefinition,然后执行判断语句if(mbd.isSingleton())。如果是单例的,则接着调用 getSingleton() 的重载方法,传入 mbd 参数。当从缓存中加载单例对象时,会把当前的单例对象在singletonObjects 中存放一份,这样可以保证在调用 getBean() 方法的时候,singletonObjects 中永远只有一个实例,在获取对象时才会给它分配内存,既保证了内存高效利用,又是线程安全的。
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "Bean name must not be null");
synchronized (this.singletonObjects) {
// 直接从缓存中获取单例Bean
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
if (this.singletonsCurrentlyInDestruction) {
throw new BeanCreationNotAllowedException(beanName,
"Singleton bean creation not allowed while singletons of this factory are in destruction " +
"(Do not request a bean from a BeanFactory in a destroy method implementation!)");
}
if (logger.isDebugEnabled()) {
logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
}
beforeSingletonCreation(beanName);
boolean newSingleton = false;
boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet<>();
}
try {
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
// Has the singleton object implicitly appeared in the meantime ->
// if yes, proceed with it since the exception indicates that state.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
if (recordSuppressedExceptions) {
for (Exception suppressedException : this.suppressedExceptions) {
ex.addRelatedCause(suppressedException);
}
}
throw ex;
}
finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 在singletonObject中添加要加载的单例
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}
如此一来,当下次需要这个单例 Bean 时,可以直接从缓存中获取。在 Spring 中创建单例的过程虽然有点绕,但是逻辑非常清楚,就是将需要的对象放在 Map 中,下次需要的时候直接从 Map 中获取即可。
3、单例模式优缺点与场景
3.1 优点
- 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
- 提供了对唯一实例的受控访问。
- 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。
- 避免对共享资源的多重占用。
3.2 缺点
- 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
3.3 使用注意事项
- 1.使用时不能用反射模式创建单例,否则会实例化一个新的对象
- 2.使用懒单例模式时注意线程安全问题
- 3.饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)
3.4 适用场景
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
- 1.需要频繁实例化然后销毁的对象。
- 2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 3.有状态的工具类对象。
- 4.频繁访问数据库或文件的对象。
以下都是单例模式的经典使用场景:
- 1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
- 2.控制资源的情况下,方便资源之间的互相通信。如线程池等。
3.5 应用场景举例
- 1.外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件
- 2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
- 3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- 6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
- 7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- 8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
- 9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
- 10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.
4、实现单利模式的原则和过程
- 1.单例模式:确保一个类只有一个实例,自行实例化并向系统提供这个实例
- 2.单例模式分类:饿单例模式(类加载时实例化一个对象给自己的引用),懒单例模式(调用取得实例的方法如getInstance时才会实例化对象)(java中饿单例模式性能优于懒单例模式,c++中一般使用懒单例模式)
- 3.单例模式要素:
- a.私有构造方法
- b.私有静态引用指向自己实例
- c.以自己实例为返回值的公有静态方法
参考文章:
https://www.cnblogs.com/zhuhuix/p/13030646.html