前言

已经介绍和学习了两个创建型模式了,今天来学习一下另一个非常常见的创建型模式,单例模式。

单例模式也被称为单件模式(或单体模式),主要作用是控制某个类型的实例数量是一个,而且只有一个。

单例模式

单例模式的实现方式

实现单例模式的方式有很多种,大体上可以划分为如下两种。

外部方式

在使用某些全局对象时,做一些“try-Use”的工作。就是如果要使用的这个全局对象不存在,就自己创建一个,把它放到全局的位置上;如果本来就有,则直接拿来使用。

内部实现方式

类型自己控制正常实例的数量,无论客户程序是否尝试过了,类型自己自己控制只提供一个实例,客户程序使用的都是这个现成的唯一实例。

目前随着集群、多核技术的普遍应用,想通过简单的类型内部控制失效真正的Singleton越来越难,试图通过经典单例模式实现分布式环境下的“单例”并不现实。所以目前介绍的这个单例是有语义限制的。

单例模式的特点

虽然单例模式也属于创建型模式,淡水它是有自己独特的特点的。

  • 单例类只有一个实例。
  • 单例类自行创建该实例,在该类内部创建自身的实例对象。
  • 向整个系统公开这个实例接口。

还有需要注意的一点,单例模式只关心类实例的创建问题,并不关心具体的业务功能。

单例模式的范围

目前Java里面实现的单例是一个ClassLoader及其子ClassLoader的范围。因为ClassLoader在装载饿汉式实现的单例类时,会响应地创建一个类的实例。这也说明,如果一个虚拟机里有多个ClassLoader(虽然说ClassLoader遵循双亲委派模型,但是也会有父加载器处理不了,然后自定义的加载器执行类加载的情况。),而且这些ClassLoader都装载着某一个类的话,就算这个类是单例,它也会产生很多个实例。如果一个机器上有多个虚拟机,那么每个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器上就有很多个实例,更不会是单例了。

还有一点再次强调,目前讨论的单例范围不适用于集群环境。

单例模式的类型

饿汉式单例

饿汉式单例是指在类被加载的时候,唯一实例已经被创建。

如下代码的例子:

/**
* 饿汉式单例模式
*
*/
public class HungrySingleton {
/**
* 定义一个静态变量用来存储实例,在类加载的时候创建,只会创建一次。
*/
private static HungrySingleton hungrySingleton = new HungrySingleton(); /**
* 私有化构造方法,禁止外部创建实例。
*/
private HungrySingleton(){
System.out.println("创建实例");
} /**
* 外部获取唯一实例的方法
* @return
*/
public static HungrySingleton getInstance(){
return hungrySingleton;
} }

懒汉式单例

懒汉式单例是指在类加载的时候不创建单例的对象,只有在第一次使用的时候创建,并且在第一次创建后,以后不再创建该类的实例。

如下代码的例子:

/**
* 懒汉式单例
*/
public class LazySingleton {
/**
* 定义一个静态变量用来存储实例。
*/
private static LazySingleton lazySingleton = null; /**
* 私有化构造方法,禁止外部创建实例。
*/
private LazySingleton(){} /**
* 外部获取唯一实例的方法
* 当发现没有初始化的时候,才初始化静态变量。
* @return
*/
public static LazySingleton getInstance(){
if(null==lazySingleton){
lazySingleton = new LazySingleton();
}
return lazySingleton;
} }

登记式单例

登记式单例实际上维护的是一组单例类的实例,将这些实例存在在一个登记薄(例如Map)中,使用已经登记过的实例,直接从登记簿上返回,没有登记的,则先登记,后返回。

如下代码例子:

/**
* 登记式单例
*/
public class RegisterSingleton { /**
* 创建一个登记簿,用来存放所有单例对象
*/
private static Map<String,RegisterSingleton> registerBook = new HashMap<>(); /**
* 私有化构造方法,禁止外部创建实例
*/
private RegisterSingleton(){} /**
* 注册实例
* @param name 登记簿上的名字
* @param registerSingleton 登记簿上的实例
*/
public static void registerInstance(String name,RegisterSingleton registerSingleton){ if(!registerBook.containsKey(name)){
registerBook.put(name,registerSingleton);
}
} /**
* 获取实例,如果在未注册时调用将返回null
* @param name 登记簿上的名字
* @return
*/
public static RegisterSingleton getInstance(String name){
return registerBook.get(name);
}
}

由于饿汉式的单例在类加载的时候就创建了一个实例,所以这个实例一直都不会变,因此也是线程安全的。但是懒汉式单例就不是线程安全的了,在懒汉式单例中有可能会出现两个线程创建了两个不同的实例,因为懒汉式单例中的getInstance()方法不是线程安全的。所以如果想让懒汉式变成线程安全的,需要在getInstance()方法中加锁。

如下所示:

   /**
* 外部获取唯一实例的方法
* 当发现没有被初始化的时候,才初始化静态变量
* @return
*/
public static synchronized LazySingleton getInstance(){
if(null==lazySingleton){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}

但是这样增加的资源消耗,延迟加载的效果虽然达到了,但是在使用的时候资源消耗确更大了,所以不建议这样用。既要实现线程安全,又要保证延迟加载。基于这样的问题就出现了另一种方式的单例模式,静态内部类式单例

静态内部类式单例

静态内部类式单例饿汉式和懒汉式的结合。

如下代码例子:

/**
* 内部静态类式单例
*/
public class StaticClassSingleton { /**
* 私有化构造方法,禁止外部创建实例。
*/
private StaticClassSingleton(){
System.out.println("创建实例了");
} /**
* 私有静态内部类,只能通过内部调用。
*/
private static class SingleClass{
private static StaticClassSingleton singleton = new StaticClassSingleton();
} /**
* 外部获取唯一实例的方法
* @return
*/
public static StaticClassSingleton getInstance(){
return SingleClass.singleton;
} }

双重检查加锁式单例

上面静态内部类的方式通过结合饿汉式和懒汉式来实现了即延迟加载了又线程安全了。下面也来介绍另一种即实现了延迟加载有保证了线程安全的方式的单例。

如下代码例子:

/**
* 双重检查加锁式单例
*/
public class DoubleCheckLockSingleton { /**
* 静态变量,用来存放实例。
*/
private volatile static DoubleCheckLockSingleton doubleCheckLockSingleton = null; /**
* 私有化构造方法,禁止外部创建实例。
*/
private DoubleCheckLockSingleton(){} /**
* 双重检查加锁的方式保证线程安全又能获得到唯一实例
* @return
*/
public static DoubleCheckLockSingleton getInstance(){
//先检查实例是否已经存在,不存在则进入代码块
if(null == doubleCheckLockSingleton){
synchronized (DoubleCheckLockSingleton.class){
//由于synchronized也是重入锁,即一个线程有可能多次进入到此同步块中如果第一次进入时已经创建了实例,那么第二次进入时就不创建了。
if(null==doubleCheckLockSingleton){
doubleCheckLockSingleton = new DoubleCheckLockSingleton();
}
}
} return doubleCheckLockSingleton;
} }

如上所示,所谓“双重检查加锁”机制,并不是每次进入getInstance()方法都需要加锁,而是当进入方法后,先检查实例是否已经存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块后,再次检查实例是否已经存在,如果不存在,就在同步块中创建一个实例,这是第二重检查。这个过程是只需要同步一次的。

还需要注意的一点是,在使用“双重检查加锁”时,需要在变量上使用关键字volatile,这个关键字的作用是,被volatile修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确地处理该变量。可能不了解Java内存模式的朋友不太好理解这句话的意思,可以去看看(JVM学习记录-Java内存模型(一)JVM学习记录-Java内存模型(二))了解一下Java内存模型,我简单说明一下,volatile这个关键字可以保证每个线程操作的变量都会被其他线程所看到,就是说如果第一个线程已经创建了实例,但是把创建的这个实例只放在了自己的这个线程中,其他线程是看不到的,这个时候如果其他线程再去判断实例是否已经存在了实例的时候,发现没有还是没有实例就会又创建了一个实例,然后也放在了自己的线程中,如果这样的话我们写的单例模式就没意义了。在JDK1.5以前的版本中对volatile的支持存在问题,可能会导致“双重检查加锁”失败,所以如果要使用“双重检查加锁”式单例,只能使用JDK1.5以上的版本。

枚举式单例

在JDK1.5中引入了一个新的特性,枚举,通过枚举来实现单例,在目前看来是最佳的方法了。Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。

还是通过代码示例来解释吧。

如下代码例子:

/**
* 单元素枚举实现单例模式
*/
public enum EnumSingleton { /**
* 必须是单元素,因为一个元素就是一个实例。
*/
INSTANCE; /**
* 测试方法1
* @return
*/
public void doSomeThing() { System.out.println("#####测试方法######");
} /**
* 测试方法2
* @return
*/
public String getSomeThing(){
return "获得到了一些内容";
}
}

上面例子中EnumSingleton.INSTANCE就可以获得到想要的实例了,调用单例的方法可以种EnumSingleotn.INSTANCE.doSomeThing()等方法。

下面来看看枚举是如何保证单例的:

首先枚举的构造方法明确是私有的,在使用枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,表明枚举实例只能被赋值一次,这样在类初始化的时候就会把实例创建出来,这也说明了枚举单例,其实是饿汉式单例方式。这样就用最简单的代码既保证了线程安全,又保证了代码的简洁。

还有一点很值得注意的是,枚举实现的单例保证了序列化后的单例安全。除了枚举式的单例,其他方式的单例,都可能会通过反射或反序列化来创建多个实例。

所以在使用单例的时候最好的办法就是用枚举的方式。既简洁又安全。

Java设计模式学习记录-单例模式的更多相关文章

  1. Java设计模式学习记录-模板方法模式

    前言 模板方法模式,定义一个操作中算法的骨架,而将一些步骤延迟到子类中.使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤. 模板方法模式 概念介绍 模板方法模式,其实是很好理解的,具体 ...

  2. Java设计模式学习记录-状态模式

    前言 状态模式是一种行为模式,用于解决系统中复杂的对象状态转换以及各个状态下的封装等问题.状态模式是将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象的状态可以灵活多变.这样在客户端使 ...

  3. Java设计模式学习记录-观察者模式

    前言 观察者模式也是对象行为模式的一种,又叫做发表-订阅(Publish/Subscribe)模式.模型-视图(Model/View)模式. 咱们目前用的最多的就是各种MQ(Message Queue ...

  4. Java设计模式学习记录-备忘录模式

    前言 这次要介绍的是备忘录模式,也是行为模式的一种 .现在人们的智能手机上都会有备忘录这样一个功能,大家也都会用,就是为了记住某件事情,防止以后自己忘记了.那么备忘录模式又是什么样子的呢?是不是和手机 ...

  5. Java设计模式学习记录-迭代器模式

    前言 这次要介绍的是迭代器模式,也是一种行为模式.我现在觉得写博客有点应付了,前阵子一天一篇,感觉这样其实有点没理解透彻就写下来了,而且写完后自己也没有多看几遍,上次在面试的时候被问到java中的I/ ...

  6. Java设计模式学习记录-解释器模式

    前言 这次介绍另一个行为模式,解释器模式,都说解释器模式用的少,其实只是我们在日常的开发中用的少,但是一些开源框架中还是能见到它的影子,例如:spring的spEL表达式在解析时就用到了解释器模式,以 ...

  7. Java设计模式学习记录-命令模式

    前言 这次要介绍的是命令模式,这也是一种行为型模式.最近反正没有面试机会我就写博客呗,该投的简历都投了.然后就继续看书,其实看书也会给自己带来成就感,原来以前不明白的东西,书上已经给彻底的介绍清楚了, ...

  8. Java设计模式学习记录-享元模式

    前言 享元模式也是一种结构型模式,这篇是介绍结构型模式的最后一篇了(因为代理模式很早之前就已经写过了).享元模式采用一个共享来避免大量拥有相同内容对象的开销.这种开销最常见.最直观的就是内存损耗. 享 ...

  9. Java设计模式学习记录-外观模式

    前言 这次要介绍的是外观模式(也称为门面模式),外观模式也属于结构型模式,其实外观模式还是非常好理解的,简单的来讲就是将多个复杂的业务封装成一个方法,在调用此方法时可以不必关系具体执行了哪些业务,而只 ...

随机推荐

  1. 6.form表单四种提交方式

    一.使用jquery的ajax方式提交: 二.使用easyui的form组件内置的submit方法提交: 三.先定义表单,然后使用submit方法提交: 四.先定义表单,然后按下enter键时提交:

  2. js加减运算·传参

    <!DOCTYPE html><html> <head> <meta charset="utf-8" /> <title> ...

  3. 怎样SQL存储过程中执行动态SQL语句

    MSSQL为我们提供了两种动态执行SQL语句的命令,分别是EXEC和sp_executesql;通常,sp_executesql则更具有优势,它提供了输入输出接口,而EXEC没有.还有一个最大的好处就 ...

  4. 二、RHCSA试题解析

    一.设置YUM仓库 YUM的软件库源地址为:http://content.example.com/rhel7.0/x86_64/dvd,将此配置为操作系统的默认软件仓库. 方法一(修改配置文件): v ...

  5. glob

    主要是用来在匹配文件,相当shell中用通配符匹配. 用法: glob.glob(pathname) # 返回匹配的文件作为一个列表返回 glob.iglob(pathname) # 匹配到的文件名, ...

  6. 在Windows7系统上能正常使用的程序,Windows10运行后部分状态不能及时变更

    这是最近在开发一个通信项目时遇到的问题,一开始以为是窗体样式的原因,把窗体换成系统窗体之后还是在Win10上不能正常使用,后面突然想到会不会是匹配原因,试了一下,结果真的就正常了. 问题:例如一个通信 ...

  7. Linux系统VIM编辑器管理(2)

    VI/VIM模式概述 在 Linux 的世界中,绝大部分的配置文件都是以 ASCII 的纯文本形态存在,因此利用简单的文字编辑软件就能够修改设定了,与微软的 Windows 系统不同的是,如果你用惯了 ...

  8. Spring Boot log4j多环境日志级别的控制

    之前介绍了在<Spring boot中使用log4j>,仅通过log4j.properties对日志级别进行控制,对于需要多环境部署的环境不是很方便,可能我们在开发环境大部分模块需要采用D ...

  9. C# 生成月份及天选择列表,方便做下拉框联动

    月份及天选择列表,很方便做下拉框联动 /// <summary> /// 获取月份选择列表(根据当前语言环境显示月份名称) /// </summary> private IEn ...

  10. html和css入门

    html 单机软件:软件程序和数据都存储在客户端 界面:Tk.PyQt.wxPython库C/S(Client/Server)架构软件:软件程序和数据一部分存在客户端,一部分存在服务器端 界面:Tk. ...