前言

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

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

单例模式

单例模式的实现方式

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

外部方式

在使用某些全局对象时,做一些“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类型的,表明枚举实例只能被赋值一次,这样在类初始化的时候就会把实例创建出来,这也说明了枚举单例,其实是饿汉式单例方式。这样就用最简单的代码既保证了线程安全,又保证了代码的简洁。

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

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

转载于:https://www.cnblogs.com/jimoer/p/9218997.html

Java设计模式学习记录-单例模式相关推荐

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

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

  2. 【一】Java 设计模式学习记录: 工厂模式

    文章目录 一.设计模式的分类 二.工厂模式 2.1 简单工厂模式 2.1.1 解决的问题 2.1.2 简单工厂模式是什么 2.1.3 优缺点 2.2 工厂方法模式 2.2.1 解决的问题 2.2.2工 ...

  3. 【三】Java 设计模式学习记录:观察者模式

    文章目录 一.观察者模式(行为型模式) 1.1 场景 1.2 普通解决方案 1.3 观察者模式定义 1.4 观察者模式原理 二.代码实现 2.1 代码结构 2.2 上代码 2.3 扩展性 三. 框架应 ...

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

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

  5. java设计模式学习笔记之装饰模式

    java设计模式学习笔记之装饰模式 尊重原创,转载请注明出处,原文地址: http://blog.csdn.net/qq137722697 这是一个使用策略模式和构建模式设计的网络请求框架,去看看吧& ...

  6. Java - 设计模式学习总结

    熟练掌握各种设计模式,并能在实际编程开发中灵活运用它们,不仅能使代码更规范,重用性更高,同时也能保证代码的可靠性,提高开发效率.这段时间又系统看了设计模式的相关内容,整理学习总结如下,文中内容如有表达 ...

  7. java设计模式-学习笔记

    java设计模式 概述 "设计模式"这个术语最初并不是出现在软件设计中,而是被用于建筑领域的设计中. <设计模式:可复用面向对象软件的基础>(Design Patter ...

  8. java设计模式系列:单例模式

    点击上方"好好学java",选择"置顶公众号" 优秀学习资源.干货第一时间送达! 好好学java java知识分享/学习资源免费分享 关注 精彩内容 java实 ...

  9. Java SE 学习记录06

    @学习记录 开始学习Java 遵从同学的指导,从Java se开始学习 黑马的JavaSE零基础入门 day06-01 面向对象 package day06;import java.util.Arra ...

最新文章

  1. 站在你身边的每个人都有可能改变世界,阿里云异构计算加速人工智能
  2. 以太坊go-ethereum项目源码本地环境搭建
  3. Attention和增强RNN (Attention and Augmented Recurrent Neural Networks)
  4. leetcode 164. 最大间距(桶排序)
  5. 人为什么总感觉莫名的心烦?
  6. python机器学习案例系列教程——层次聚类(文档聚类)
  7. 解析函数的幂级数理论【洛朗展开(Laurent 展开)】
  8. 没有找到MSVCR100.dll解决方法
  9. bootbox的使用
  10. CodeForces 595A Vitaly and Night
  11. 密室逃脱实体店怎么吸引客户?这几招教你实现线上引流转化!
  12. laravel中提供DB facade(原始查找)、查询构造器、Eloquent ORM三种操作数据库方式
  13. 教你一招快速清理DNS缓存
  14. python对excel筛选提取文本中数字_详解利用python提取pdf文本数字
  15. daytime协议的服务器和客户端程序,用Socket套接字实现DAYTIME协议的服务器和客户端程序-20210414073352.docx-原创力文档...
  16. 【web开发】利用User-Agent获取浏览器类型
  17. 专精特新中小企业认定标准
  18. 回文子串是什么意思?
  19. SLAM导航机器人零基础实战系列:(五)树莓派3开发环境搭建——1.安装系统ubuntu_mate_16.04...
  20. 使用 ASM 编写 Java 字节码混淆器以实现对代码的保护

热门文章

  1. mt6765和骁龙665哪个好_骁龙665+5000毫安大电池不到千元,手机厂商还要怎么玩?...
  2. toadstool sql格式化
  3. 【clickhouse】flink jdbc 方式写入 clickhouse 报错 request to {}->http://xxx:8123: Broken pipe
  4. 【SpringCloud】Spring cloud Alibaba Nacos 集群和持久化配置
  5. 【kafka】kafka 2.3 关于控制Broker端入站连接数的讨论
  6. 【Elasticsearch】es 各种 日志 慢日志 慢查询
  7. 95-240-050-原理-State-RocksDBStateBackend
  8. 【Flink】源码-Flink重启策略-简介 Task恢复策略 重启策略监听器
  9. 【Antlr】WHITESPACE is not a recognized channel name
  10. 20-190-092-安装-Flink集群安装 flink-1.9.0 On Yarn