最简单的设计模式——单例模式的演进和推荐写法(Java 版)
前言
如下是之前总结的 C++ 版的;软件开发常用设计模式—单例模式总结(c++版),对比发现 Java 实现的单例模式和 C++ 的在线程安全上还是有些区别的。
概念不多说,没意思,我自己总结就是:
有这样一个类,该类在生命周期内有且只能有一个实例,该类必须自己创建自己的这个唯一实例,该类必须给所有其他对象提供这一实例(提供全局访问点),这样的类就叫单例类。
简单的说就是满足三个条件:
1、生命周期内有且只能有一个实例
2、自己提供这个独一无二的实例
3、该实例必须是能全局访问的
需要的考虑的细节
进一步,单例类,最好能实现懒加载,随用随生成,而不是初始化的时候就生成,提高启动速度和优化内存。
还有应该考虑并发环境下的场景,多线程的单例模式实现有什么难点,回答这个问题,必须先知道Java的内存模型,参考:JVM学习(3)——总结Java内存模型
考虑黑客会做反序列化的攻击
考虑黑客会做反射的攻击,因为反射可以访问私有方法
。。。
单线程环境下懒加载的单例
如果程序确认没有多线程的使用场景,完全可以简单一些写。
public class NoThreadSafeLazySingleton {
private static NoThreadSafeLazySingleton lazySingleton = null;
private NoThreadSafeLazySingleton() {
}
public static NoThreadSafeLazySingleton getLazySingleton() {
if (lazySingleton == null) {
lazySingleton = new NoThreadSafeLazySingleton();
}
return lazySingleton;
}
}
很简单,但是只适用于单线程环境
线程安全的懒加载单例
原理也很简单,没什么可说的,如下示例代码:
public class ThreadSafeLazySingleton {
private static volatile ThreadSafeLazySingleton lazySingleton = null;
private ThreadSafeLazySingleton() {
}
public static ThreadSafeLazySingleton getLazySingleton() {
if (lazySingleton == null) {
synchronized (ThreadSafeLazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new ThreadSafeLazySingleton();
}
}
}
return lazySingleton;
}
}
主要是注意 volatile 关键字的使用,否则这种所谓双重检查的线程安全的单例是有 bug 的。参考:JVM学习(3)——总结Java内存模型
静态内部类方案
在某些情况中,JVM 隐含了同步操作,这些情况下就不用自己再来进行同步控制了。这些情况包括:
由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
访问final字段时
在创建线程之前创建对象时
线程可以看见它将要处理的对象时
在静态内部类里去创建本类(外部类)的对象,这样只要不使用这个静态内部类,那就不创建对象实例,从而同时实现延迟加载和线程安全。
public class Person {
private String name;
private Integer age;
private Person() {
}
private Person(String name, Integer age) {
this.name = name;
this.age = age;
}
// 在静态内部类里去创建本类(外部类)的对象
public static Person getInstance() {
return Holder.instatnce;
}
// 静态内部类相当于外部类 Person 的 static 域,它的对象与外部类对象间不存在依赖关系,因此可直接创建。
// 因为静态内部类相当于其外部类 Person 的静态成员,所以在第一次被使用的时候才被会装载,且只装载一次。
private static class Holder {
// 内部类的对象实例 instatnce ,是绑定在外部 Person 对象实例中的
// 静态内部类中可以定义静态方法,在静态方法中只能够引用外部类中的静态成员方法或者成员变量,比如 new Person
// 使用静态初始化器来实现线程安全的单例类,它由 JVM 来保证线程安全性。
private static final Person instatnce = new Person("John", 31);
}
}
静态内部类相当于外部类 Person 的 static 域(静态成员),它的对象与外部类对象间不存在依赖关系,因此可直接创建。
既然,静态内部类相当于其外部类 Person 的静态成员,所以在第一次被使用的时候才被会装载,且只装载一次,实现了懒加载和单例。
而且,使用静态初始化器来实现单例类,是线程安全的,因为由 JVM 来保证线程安全性
客户端调用
Person person = Person.getInstance();
该方案实现了,线程安全的单例 + 懒加载的单例,但是并不能防反序列化攻击,需要额外的加以约束。
反序列化攻击单例类
其实这个 case 没必要说太多,知道就行,因为哪里就这么巧,一个能序列化的类(实现了Serializable/Externalizable接口的类),就恰恰是单例的呢?
看下面例子,把 Person 类改造为能序列化的类,然后用反序列攻击单例
public class SerializationTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = Person.getInstance();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("person"));
objectOutputStream.writeObject(person);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("person"));
Person person1 = (Person) objectInputStream.readObject();
System.out.println(person == person1); // false
}
}
比较两个 person 实例地址,是 false,说明生成了两个对象,违背了单例类的初衷,那么为了能在序列化过程仍能保持单例的特性,可以在Person类中添加一个readResolve()方法,在该方法中直接返回Person的单例对象
public Object readResolve() {
return Holder.instatnce;
}
原理是当从 I/O 流中读取对象时,ObjectInputStream 类里有 readResolve() 方法,该方法会被自动调用,期间经过种种逻辑,最后会调用到可序列化类里的 readResolve()方法,这样可以用 readResolve() 中返回的单例对象直接替换在反序列化过程中创建的对象,实现单例特性。
也就是说,无论如何,反序列化都会额外创建对象,只不过使用 readResolve() 方法可以替换之。
具体有关Java 对象的序列化可以参考笔记:深入理解Java对象序列化
反射攻击单例类
直接看例子,做法很简单,通过 Java 的反射机制,看看能不能拿到单例类的私有构造器,并且改变构造器的访问属性
public class ReflectTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException {
Person person = Person.getInstance();
Class clazz = Class.forName("com.dashuai.D13Singleton.Person");
Constructor constructor = clazz.getDeclaredConstructor();
// constructor.setAccessible(true);
Person person1 = (Person) constructor.newInstance();
System.out.println(person == person1); // false
}
}
运行抛出了异常:

但是,如果把注释的行打开,就不会出错,且打印 false。
网上有一些解决方案,比如在构造器里加判断,如果二次调用就抛出异常,其实也没从根本上解决问题。
解决所有问题的方案——枚举实现单例类
目前公认的最佳方案,代码极少,线程安全,防止反射和序列化攻击
public enum EnumSingleton {
ENUM_SINGLETON;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
////////////////////////调用
EnumSingleton.ENUM_SINGLETON.setName("dashuai");
System.out.println(EnumSingleton.ENUM_SINGLETON.getName());
所有的变量都是单例的。至于为什么,可以通过反编译工具查看枚举的源码。可以安装 idea 的 jad 插件,会发现就是按照单例模式设计的。
享元模式和单例模式的异同
享元模式是对象级别的, 也就是说在多个使用到这个对象的地方都只需要使用这一个对象即可满足要求。
单例模式是类级别的, 就是说这个类必须只能实例化出来一个对象。
可以这么说, 单例是享元的一种特例, 设计模式不用拘泥于具体代码, 代码实现可能有n多种方式, 而单例可以看做是享元的实现方式中的一种, 他比享元模式更加严格的控制了对象的唯一性
使用单例的场景和条件是什么?
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
JDK里有哪些使用了单例模式的例子?说明之
java.lang.runtime getRuntime,代码也很简单。
欢迎关注
dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!

最简单的设计模式——单例模式的演进和推荐写法(Java 版)的更多相关文章
- Head First 设计模式系列之二----备忘录模式(java版)
申明:这几天无意中关注到备忘录模式,比较陌生回家一番参考书,只在附录里记录了该模式.后来在园子里有发现了有专门写设计模式的博客,并且写的也得牛逼.附上链接 http://www.cnblogs.com ...
- Singleton单例模式是最简单的设计模式,它的主要作用是保证在程序执行生命周期中,使用了单类模式的类仅仅能有一个实例对象存在。
...
- GOF业务场景的设计模式-----单例模式
个人觉得 纯粹的学习设计模式,是不对的.也不能为了使用设计模式,而硬搬设计模式来使用 单例模式可能是 最简单的设计模式也是 大家知道最多的设计模式.当然 ,有很多种写法 定义:确保一个类只有一个实例, ...
- 浅谈设计模式--单例模式(Singleton Pattern)
题外话:好久没写blog,做知识归纳整理了.本来设计模式就是个坑,各种文章也写烂了.不过,不是自己写的东西,缺少点知识的存在感.目前还没做到光看即能记住,得写.所以准备跳入设计模式这个大坑. 开篇先贡 ...
- JavaScript设计模式-单例模式、模块模式(转载 学习中。。。。)
(转载地址:http://technicolor.iteye.com/blog/1409656) 之前在<JavaScript小特性-面向对象>里面介绍过JavaScript面向对象的特性 ...
- dom4j解析器 基于dom4j的xpath技术 简单工厂设计模式 分层结构设计思想 SAX解析器 DOM编程
*1 dom4j解析器 1)CRUD的含义:CreateReadUpdateDelete增删查改 2)XML解析器有二类,分别是DOM和SAX(simple Api for xml). ...
- 设计模式 单例模式(Singleton) [ 转载2 ]
设计模式 单例模式(Singleton) [ 转载2 ] @author java_my_life 单例模式的结构 单例模式的特点: 单例类只能有一个实例. 单例类必须自己创建自己的唯一实例. 单例类 ...
- 设计模式 单例模式(Singleton) [ 转载 ]
设计模式 单例模式(Singleton) [ 转载 ] 转载请注明出处:http://cantellow.iteye.com/blog/838473 前言 懒汉:调用时才创建对象 饿汉:类初始化时就创 ...
- c#设计模式-单例模式(面试题)
c#设计模式-单例模式 单例模式三种写法: 第一种最简单,但没有考虑线程安全,在多线程时可能会出问题, public class Singleton { private static Singleto ...
随机推荐
- 英语口语练习系列-C26-广告-人际关系-辨别物体-如果
词汇-广告 advertisement noun [ C ] UK /ədˈvɜː.tɪs.mənt/ US /æd.vɝːˈtaɪz.mənt/ informal ad, uk also infor ...
- Idea创建一个springboot多模块项目
一.创建空Maven项目 二.左边选择maven,右边可以什么不选,直接next: 三.填写artifactId,点击next直到finish 四.finish后,idea会生成如下结果模块,删除sr ...
- 简单酷炫的Canvas数字时钟
声明:本文为原创文章,如需转载,请注明来源WAxes,谢谢! 我记得很早之前就看过这个DEMO,是岑安大大博客里看到的: 就是这个数字时钟,当时觉得这个创意不错,但是也没去折腾.直到昨天同事又在网上看 ...
- 考前停课集训 Day1 废
[友情链接] Day1 今天模拟赛倒数…… 感觉自己菜到爆炸…… 被一个以前初一的倒数爆踩…… 感觉自己白学了. 满分400,自己只有100.真的是倒数第一…… 做了一个T2,其他暴力分全部没有拿到… ...
- mybatis查询到count(*)返回值
resultType="int" <select id="num_List_" parameterType=" xxx" result ...
- ./configure && make && make install 编译安装和卸载 (Linux)
./configure && make && make install 编译安装和卸载 (Linux) 正常的编译安装/卸载: 源码的安装一般由3个步骤组成:配置( ...
- 用node搭建本地服务环境
const express = require('express'); const path = require('path'); const request = require('request') ...
- 开源流媒体服务器SRS学习笔记(3) - HTTPCallback实现安全认证
按上回继续,安全论证是绝大多数应用的基本要求,如果任何人都能无限制的发布/播放视频,显然不适合.SRS中可以通过HTTPCallback机制来实现,参考下面的配置: ... vhost __defau ...
- python3 读取dbf文件报错 UnicodeDecodeError: 'gbk' codec can't decode
在读取dbf文件时由于编码问题报错:UnicodeDecodeError: 'gbk' codec can't decode byte 0xb5 in position 49: incomplete ...
- 使用python实现深度神经网络 4(转)
https://blog.csdn.net/oxuzhenyi/article/details/73026807 使用浅层神经网络识别图片中的英文字母 一.实验介绍 1.1 实验内容 本次实验我们正式 ...