单例模式作为一种创建型模式,在日常开发中用处极广,我们先来看一一段代码:

// 构造函数
protected Calendar(TimeZone var1, Locale var2) {
this.lenient = true;
this.sharedZone = false;
this.nextStamp = 2;
this.serialVersionOnStream = 1;
this.fields = new int[17];
this.isSet = new boolean[17];
this.stamp = new int[17];
this.zone = var1;
this.setWeekCountData(var2);
} // 提供 Calendar 类实例的方法
public static Calendar getInstance(){
return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}

看过上一篇博客Java设计模式(5:工厂模式详解)的朋友应该熟悉这段来自JDKCalendar类的代码,这就是单例模式的一种实现:

  1. Calendar类的构造函数被protected修饰,保证其不能被其他包下的类访问。
  2. getInstance()方法提供了获得Calendar类实例化对象的方法。

从上述代码来看,我们可以认定实现单例模式需要满足两个基本原则:

  1. 类的构造函数私有化。
  2. 该类需要提供一个获得实例的全局访问点

所以可以得出结论:单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局的访问点。

得出结论后,再来看看实现。在java语言当中,两种方式构建单例模式:饿汉式单例懒汉式单例

一、饿汉式单例

// 饿汉式单例
public class HungrySingleton {
// 构造函数私有化
private HungrySingleton() {} private static final HungrySingleton singleton = new HungrySingleton(); // 提供一个全局的访问点
public static HungrySingleton getInstance(){
return singleton;
}
}

饿汉式单例是在类加载的时候就立即初始化,并且创建了单例对象。在上述代码中,当HungrySingleton类在被类加载器加载时,它的实例对象singleton就已经创建完成了;并且根据类的加载机制,我们明白:singleton作为HungrySingleton类中的一个静态的声明对象,在HungrySingleton类第一次被类加载器加载时就已经创建完成,并且只会创建这一次。这就保证了无论getInstance()方法被调用多少次,返回的都是同一个singleton实例;保证了线程的绝对安全,不会出现访问安全的问题。

但也正式因为singleton实例在HungrySingleton类第一次被类加载器加载时就已经创建完成,若getInstance()方法不被任何地方调用,那么singleton实例就会一直占着内存空间,白白浪费了资源。所以引申出了另一种构建单例模式的方式:懒汉式单例

二、懒汉式单例

懒汉式单例的特点是只有在类的全局访问点被访问的时候,类的实例化对象才会创建

// 懒汉式单例
public class LazySingleton { // 构造函数私有化
private LazySingleton() {} private static LazySingleton lazySingleton = null; // 全局访问点
public static LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}

在上述代码中,只有当getInstance()方法被调用时,才会去创建lazySingleton实例。这样就解决了饿汉式模式中的资源占用问题,但同样引申出了另一个问题:线程安全问题

我们先来创建一个属于我们自己的线程类LazyThread

// 线程
public class LazyThread implements Runnable {
@Override
public void run() {
LazySingleton instance = LazySingleton.getInstance();
// 打印 线程名字 和 instance实例的内存地址
System.out.println(Thread.currentThread().getName() + ":" +instance);
}
}

调用:

//  创建两个线程
public static void main(String[] args) {
Thread thread1 = new Thread(new LazyThread());
Thread thread2 = new Thread(new LazyThread());
thread1.start();
thread2.start();
}

我们采用debug模式调试一下,先和下图一般,在LazySingleton类中打一个断点

再用鼠标右键点击断点的位置(红色圆点的位置),打开如下图的框之后,先选择红框中的Thread模式,再点击蓝框中的Done按钮。

做完上述的操作之后,我们来用debug模式运行一下main方法

上图红框中内容就是我们所创建的两个线程,目前是Thread-0线程在运行。我们将Thread-0线程运行到lazySingleton = new LazySingleton()这行代码的位置(图1),然后切换为Thread-1线程,并将Thread-1线程同样运行到此位置(图2):

图1:

图2:

最后:切换回Thread-0线程,并全部放开,让代码一直运行下去;并对Thread-1做出同样的操作。打印出结果:

通过结果可以看出,两个线程获得的lazySingleton实例所对应的内存地址不相同,显然不符合单例模式中的只有一个实例的原则。

那有什么办法可以保证懒汉式模式在线程环境下安全呢?有,而且很简单,加锁。我们来给getInstance()方法加上锁:

// 懒汉式
public class LazySingleton { // 私有化构造函数
private LazySingleton() {} private static LazySingleton lazySingleton = null; // 加锁
public synchronized static LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}

我们再用上述的方式来debug调试一下:

在线程Thread-1进入getInstance()方法内部的时候,线程Thread-0处于MONITOR锁监控的状态。将线程Thread-1运行完后,Thread-0进入getInstance()方法内部,状态更新为RUNNING运行状态。

而此时我们可以看出lazySingleton已经有值了,所以我们将线程Thread-0运行完后,两个线程会打印出一样的结果:

由结果我们可以看出,在给getInstance()方法加上锁之后,线程安全的问题便解决了。但依然可以继续来优化这段懒汉式单例模式的代码。

// 懒汉式
public class LazySingleton { // 私有化构造函数
private LazySingleton() {} // volatile 关键字 解决重排序的问题
private volatile static LazySingleton lazySingleton = null; public static LazySingleton getInstance(){
if (lazySingleton == null){
// 锁代码块
synchronized (LazySingleton.class) {
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}

这种方式被称为双重检查锁,它有着以下两点的好处:

  1. 线程由基于LazySingleton整个类的阻塞变为在getInstance()方法内部的阻塞。锁的颗粒度变得更细,锁的代码块变得更小了。
  2. 第一重的if判断,直接分流了一部分在lazySingleton实例化后在进入getInstance()方法的线程,提高了效率。

但是,只要涉及到加锁的问题,对程序的性能或多或少都有影响,那么有没有不加锁的方式呢?当然也是有的,那就是以类的初始化角度来考虑,使用内部类的方式。

三、静态内部类实现单例模式

// 懒汉式模式 和 饿汉式模式 兼顾
public class InnerClassSingleton { // 私有化构造函数
private InnerClassSingleton(){} public static InnerClassSingleton getInstance(){
return SingletonHolder.singleton;
} // 静态内部类
private static class SingletonHolder{
private static final InnerClassSingleton singleton = new InnerClassSingleton();
}
}

这种方式兼顾了懒汉式模式饿汉式模式,根据类的加载机制来说,静态内部类SingletonHolder不会随着外部类InnerClassSingleton的加载而加载,只会在被调用时才会加载。

这里外部类InnerClassSingleton在被类加载器加载后,并不会去进一步加载SingletonHolder类,从而也不会去实例化singleton,也就避免了资源浪费的情况。而在getInstance()方法第一次被调用时,内部类SingletonHolder才会加载,SingletonHolder类中声明的静态对象singleton才会被实例化;后面每一次调用getInstance()方法时,返回的都是此singleton对象,保证了只有一个实例化对象的原则。

四、用反射的方式来破坏单例

讲完单例模式的几种实现方式之后,我们来讲一讲破坏单例的方式;虽然日常开发中不会怎么用到,但对面试来说,可以说是一个必考点。多了解了解,总会有意想不到的用处。

public static void main(String[] args) {
try {
// 用反射获得 InnerClassSingleton 类的实例
Class clazz = InnerClassSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
// 强制访问
constructor.setAccessible(true);
InnerClassSingleton instance1 = (InnerClassSingleton)constructor.newInstance(); // 单例模式获取
InnerClassSingleton instance2 = InnerClassSingleton.getInstance(); System.out.println("利用反射得到的实例对象:"+instance1);
System.out.println("单例模式的实例对象:"+instance2);
}catch (Exception e){
e.printStackTrace();
}
}

上述的测试代码,我分别用反射的方式和单例的方式来获得InnerClassSingleton类的实例,最后打印出来,看一看结果:

可以看出,两次创建的InnerClassSingleton类的实例又不相同了。那怎么杜绝这种办法呢?我们可以来优化一下上述的静态内部类的代码:

// 懒汉式模式 和 饿汉式模式 兼顾
public class InnerClassSingleton { // 私有化构造函数
private InnerClassSingleton(){
if (SingletonHolder.singleton != null){
throw new RuntimeException("不能以这种方式来获得实例对象......");
}
} public static InnerClassSingleton getInstance(){
return SingletonHolder.singleton;
} // 静态内部类
private static class SingletonHolder{
private static final InnerClassSingleton singleton = new InnerClassSingleton();
}
}

主要看私有构造函数中的代码,我们将这里做了限制,当被外界调用时,直接抛出异常!测试的结果也如我们所愿:

五、用序列化的方式破坏单例

除了反射之外,用序列化的方式也能破坏单例,达到创建不一样的类的实例的效果。

先将InnerClassSingleton类实现序列化接口:

// 懒汉式模式 和 饿汉式模式 兼顾
public class InnerClassSingleton implements Serializable {
// ....... 中间的代码查看上面的代码
}

编写测试代码:

public static void main(String[] args) {
try {
InnerClassSingleton instance1 = InnerClassSingleton.getInstance(); FileOutputStream fos = new FileOutputStream("singleton.obj");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos);
objectOutputStream.writeObject(instance1);
objectOutputStream.flush();
objectOutputStream.close();
fos.close(); FileInputStream fis = new FileInputStream("singleton.obj");
ObjectInputStream objectInputStream = new ObjectInputStream(fis);
InnerClassSingleton instance2 = (InnerClassSingleton)objectInputStream.readObject();
objectInputStream.close();
fis.close(); System.out.println("利用单例获得实例:"+instance1);
System.out.println("利用序列化获取的实例:"+instance2);
}catch (Exception e){
e.printStackTrace();
}
}

在上面的代码中,我们先获得InnerClassSingleton类的实例instance1,再将instance1写入singleton.obj文件当中;然后再从中取出来,转化为实例instance2;最后将instance1instance2打印出来:

可以看出,两次创建的InnerClassSingleton类的实例又不相同了。那么这种方式的解决方案是什么呢?也不难,只需要加上一个方法就好了:

public class InnerClassSingleton implements Serializable {
// ....... 代码省略
// 加上 readResolve() 方法
private Object readResolve(){
return SingletonHolder.singleton;
} // 静态内部类
private static class SingletonHolder{
private static final InnerClassSingleton singleton = new InnerClassSingleton();
}
}

再加上readResolve()之后,再来测试一下:

可以看出,两次创建的实例完全相同,完美的解决了序列化的问题。那么为什么加上readResolve()就会解决这个问题呢?这里和JDK的源码有关,我这里就不贴源码了,不便于观看,我这里画了一个时序图,大家可以跟着这个时序图来对照JDK源码进行查看,了解内情。

1、先从编写的测试代码里面进入ObjectInputStream类中的readObject()方法

2、实序图

以实序图来看,其实方法内部还是创建了一次InnerClassSingleton类的实例,不过是后面用调用readResolve()方法获得的InnerClassSingleton类的实例将它替换掉了,所以打印出的结果依旧是相同的。总体来说,还是白白消耗了内存,那么再来看另一种创建单例的方式。

六、注册式单例

注册式单例又被称为登记式单例,大体分为枚举登记容器缓存两种。

6.1 枚举登记

public enum  EnumSingleton {

    INSTANCE;

    // 用来测试对象是否相同
private Object data; public Object getData() {
return data;
} public void setData(Object data) {
this.data = data;
} public static EnumSingleton getInstance(){
return INSTANCE;
}
}
6.1.1 序列化破坏

将上面的测试代码稍微更改一下:

public static void main(String[] args) {
try {
EnumSingleton instance1 = EnumSingleton.getInstance(); instance1.setData(new Object());
// ....... 查看 五、用序列化的方式破坏单例 的测试代码
EnumSingleton instance2 = (EnumSingleton)objectInputStream.readObject();
objectInputStream.close();
fis.close(); System.out.println("利用单例获得实例:"+instance1.getData());
System.out.println("利用序列化获取的实例:"+instance2.getData());
}catch (Exception e){
e.printStackTrace();
}
}

结果:

由结果可以看出是可行的,那么原理是什么呢?通过上述实序图的方式查看源码:

1、ObjectInputStream类中的readObject0()方法:

private Object readObject0(boolean unshared) throws IOException {
// ...... 省略代码
// 如果是枚举类
case TC_ENUM:
return checkResolve(readEnum(unshared)); // ......
}

2、readEnum()方法

private Enum<?> readEnum(boolean unshared) throws IOException {
// ......
if (cl != null) {
try {
// 通过Class对象 c1 和 类名 name 来获得唯一的枚举对象
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
// ......
}

通过查看源码发现,枚举类型其实通过Class 对象类和类名找到一个唯一的枚举对象;因此,枚举对象不可能被类加载器加载多次。

6.1.2 反射破坏

测试代码:

public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
// 强制访问
constructor.setAccessible(true);
EnumSingleton instance1 = (EnumSingleton)constructor.newInstance();
EnumSingleton instance2 = EnumSingleton.getInstance();
System.out.println("利用反射得到的实例对象:"+instance1);
System.out.println("单例模式的实例对象:"+instance2);
}catch (Exception e){
e.printStackTrace();
}
}

结果:

它竟然报出java.lang.NoSuchMethodException,意思是没有找到对应的无参的构造函数,这是为什么呢?不急,让我们将EnumSingleton.class这个文件反编译一下(这里使用的是jad反编译工具,不会的同学去网上搜教程,这里不详细讲解了),得到一个EnumSingleton.jad文件,打开文件后发现这么一段代码:

// .....
private EnumSingleton(String s, int i){
super(s, i);
} // .....
static {
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}

原来jvm在编译EnumSingleton枚举类时,给它创建了一个有参的构造函数,并再静态代码块里面实例化了INSTANCE对象。那这里,我们再将测试代码修改一下,强制传入两个参数会怎么样:

public static void main(String[] args) {
try {
Class clazz = EnumSingleton.class;
// 设置两个参数的类型
Constructor constructor = clazz.getDeclaredConstructor(String.class,int.class);
// 强制访问
constructor.setAccessible(true);
// 传入两个参数
EnumSingleton instance1 = (EnumSingleton)constructor.newInstance("test",111);
EnumSingleton instance2 = EnumSingleton.getInstance();
System.out.println("利用反射得到的实例对象:"+instance1);
System.out.println("单例模式的实例对象:"+instance2);
}catch (Exception e){
e.printStackTrace();
}
}

结果:

还是报错,不过这次的错误换成了Cannot reflectively create enum objects,不允许创建枚举类的对象。我们来看看JDK的源码:

constructor.newInstance("test",111)这行代码进入Constructor类中的newInstance()方法我们发现,这里有个判断,如果是对枚举类进行操作,那么直接报出错误;这么看来,是JDK源码帮助我们去拦截了来自反射技术的破坏,那么就可以放宽心了。

6.2 容器缓存

容器缓存最经典的例子就是Spring框架中的IOC容器,我们来模拟一下:

// 容器缓存
public class ContainerSingleton { // 私有化构造函数
private ContainerSingleton(){} private static Map<String,Object> iocMap = new ConcurrentHashMap<>(); // 传入 类名参数
public static Object getBean(String className){
if (className == null || "".equals(className)){
return null;
}
synchronized (iocMap){
// 判断容器中是否有该属性
if (!iocMap.containsKey(className)){
Object object = null;
try {
object = Class.forName(className).newInstance();
iocMap.put(className,object);
}catch (Exception e){
e.printStackTrace();
}
return object;
} else {
return iocMap.get(className);
}
}
}
}

iocMap中的key存的是类名,value存的是该类的实例化对象,通过这种方式来保证每次获得的都是一个类的相同实例。

七、ThreadLocal线程单例

ThreadLocal方式创建的单例对象是最为特殊的,因为它是一个伪单例,它只能保证同一个线程内创建的类的实例是相同的,有着天生的线程安全;但不能保证创建的类的实例是全局唯一的;先来看看代码:

public class ThreadLocalSingleton {

    public ThreadLocalSingleton() {}

    private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal(){
@Override
protected Object initialValue() {
return new ThreadLocalSingleton();
}
}; public static ThreadLocalSingleton getInstance(){
return threadLocal.get();
}
}

线程代码:

public class LazyThread implements Runnable {
@Override
public void run() {
ThreadLocalSingleton instance = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" +instance);
}
}

测试代码:

public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" +ThreadLocalSingleton.getInstance());
System.out.println("————————————————————————————————————————");
Thread thread1 = new Thread(new LazyThread());
Thread thread2 = new Thread(new LazyThread());
thread1.start();
thread2.start();
}

结果:

从结果可以看出,再主线程main中,无论我们调用多少次getInstance()方法,获得的ThreadLocalSingleton的实例都是相同的。而两个子线程Thread-0Thread-1都获得了不同的实例。那么这是怎么做到了呢?

通过查看源码(别问我为啥不贴源码,问就是看不到,它的底层不是用Java写的【流泪】,感兴趣的小伙伴可以百度,有大神,我也是百度的,yyds!!!)我们发现,ThreadLocalThreadLocalSingleton类的实例对象全部放在了ThreadLocalMap中,为每一个线程提供了对象,实际上是以空间换时间来实现线程隔离的。这也使ThreadLocal技术频繁的使用了于用户登陆时,存储用户的登录信息方面。甚至于Mybatis中多个数据源切换的技术也是用它实现的。

最后


如果这篇文章对你有帮助的话,麻烦动动小手,点个赞,万分感谢!!!

如果有小伙伴发现文章里面有错误,欢迎来指正,不胜感激!!!

JAVA设计模式(6:单例模式详解)的更多相关文章

  1. Java设计模式之单例模式详解

    在Java开发过程中,很多场景下都会碰到或要用到单例模式,在设计模式里也是经常作为指导学习的热门模式之一,相信每位开发同事都用到过.我们总是沿着前辈的足迹去做设定好的思路,往往没去探究为何这么做,所以 ...

  2. Java设计模式-策略模式详解

    前言 在软件领域中,设计模式作为一种经典的开发实践常常需要我们去深入的理解,而策略模式作为设计模式的一种,使用频率也是相对来说比较高的,在Java中,当我们学习TreeSet集合的时候,就采用了经典的 ...

  3. Java双重校验单例模式详解

    单例模式双重检测java实现: public class Singleton { private volatile static Singleton instance = null; //#1 pub ...

  4. 9种Java单例模式详解(推荐)

    单例模式的特点 一个类只允许产生一个实例化对象. 单例类构造方法私有化,不允许外部创建对象. 单例类向外提供静态方法,调用方法返回内部创建的实例化对象.  懒汉式(线程不安全) 其主要表现在单例类在外 ...

  5. Java 8 Stream API详解--转

    原文地址:http://blog.csdn.net/chszs/article/details/47038607 Java 8 Stream API详解 一.Stream API介绍 Java8引入了 ...

  6. java反射机制深入详解

    java反射机制深入详解  转自:http://www.cnblogs.com/hxsyl/archive/2013/03/23/2977593.html 一.概念 反射就是把Java的各种成分映射成 ...

  7. 国际化,java.util.ResourceBundle使用详解

    java.util.ResourceBundle使用详解   一.认识国际化资源文件   这个类提供软件国际化的捷径.通过此类,可以使您所编写的程序可以:          轻松地本地化或翻译成不同的 ...

  8. java之StringBuffer类详解

    StringBuffer 线程安全的可变字符序列. StringBuffer源码分析(JDK1.6): public final class StringBuffer extends Abstract ...

  9. java.util.ResourceBundle使用详解

    java.util.ResourceBundle使用详解   一.认识国际化资源文件   这个类提供软件国际化的捷径.通过此类,可以使您所编写的程序可以:          轻松地本地化或翻译成不同的 ...

  10. java之AbstractStringBuilder类详解

    目录 AbstractStringBuilder类 字段 构造器 方法   public abstract String toString() 扩充容量 void  expandCapacity(in ...

随机推荐

  1. SQLFlow使用中的注意事项--设置篇

    SQLFlow 是用于追溯数据血缘关系的工具,它自诞生以来以帮助成千上万的工程师即用户解决了困扰许久的数据血缘梳理工作. 数据库中视图(View)的数据来自表(Table)或其他视图,视图中字段(Co ...

  2. 一道VM的逆向所引发的符号执行思路

    逆向虚拟机保护 虚拟机保护类的题目需要找到虚拟机的vm_code(字节码),各个handler,然后进一步分析虚拟机保护代码的流程. 用IDA打开程序,经分析后0x403040全局变量地址处存储的就是 ...

  3. 事后分析$\alpha$

    项目 内容 课程:北航-2020-春-软件工程 博客园班级博客 要求 事后分析 我们在这个课程的目标是 提升团队管理及合作能力,开发一项满意的工程项目 这个作业在哪个具体方面帮助我们实现目标 组织组员 ...

  4. I/O流以及文件的基本操作

    文件操作: 文件操作其实就是一个FIle类:我们学习文件操作就是学习File类中的方法: 文件基操: 第一部分:学习文件的基本操作(先扒源码以及文档) Constructor Description ...

  5. [Django框架之视图层]

    [Django框架之视图层] 视图层 Django视图层, 视图就是Django项目下的views.py文件,它的内部是一系列的函数或者是类,用来专门处理客户端访问请求后处理请求并且返回相应的数据,相 ...

  6. curl -O http://www.linux.com/hello.sh

    2.3:可以使用curl的内置option:-O(大写)保存网页中的文件要注意这里后面的url要具体到某个文件,不然抓不下来 # curl -O http://www.linux.com/hello. ...

  7. 【例 3】 修改 bols 文件的 atime 和 mtime。 [root@localhost ~]# touch -d "2017-05-04 15:44" bols

    Linux touch命令:创建文件及修改文件时间戳 < Linux删除空目录(rmdir命令)Linux在文件之间建立软/硬链接(ln命令) > <Linux就该这么学>是一 ...

  8. canal 环境搭建 canal 与kafka通信(三)

    canal 占用了生产者 .net core端 使用消费者获取canal 消息 安装 Confluent.Kafka  demo使用 1.3.0 public static void Consumer ...

  9. vimdiff env.txt export.txt set.txt

    1. 环境变量 简单理解了变量的概念,就很容易理解环境变量了.环境变量的作用域比自定义变量的要大,如 Shell 的环境变量作用于自身和它的子进程.在所有的 UNIX 和类 UNIX 系统中,每个进程 ...

  10. Hutool :一个小而全的 Java 工具类库

    Hutool 简介 Hutool 是一个小而全的 Java 工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以"甜甜的 ...