headfirst设计模式(6)—单例模式
前言
这一章的课题看起来就很和蔼可亲了,比起前面绕的我不要不要的工厂模式,那感觉真是太好了,但是正是因为简单,那么问题就来了,我怎么才能把这个东西叙述清楚?怎么样才能老少咸宜呢?
如何能够在把这个东西讲清楚的同时,引入一些新的东西让这个设计模式能显得不那么普通呢?我不知道能不能做到,不过,吹x马上开始
首先,还是贴一波HeadFirst源码地址:
github地址:https://github.com/bethrobson/Head-First-Design-Patterns
单例入门浅析
HeadFirst的原文是由一个巧克力锅炉的例子引入了经典的单例模式,具体例子不赘述,直接进入经典单例模式的贴代码环节(注意:以下所有代码为了方便区分和源代码稍有不同)
经典的单例模式(线程不安全):
public class ClassicSingleton {
private static ClassicSingleton UNIQUE_INSTANCE;
private ClassicSingleton() {
}
public static ClassicSingleton getInstance() {
if (UNIQUE_INSTANCE == null) {
UNIQUE_INSTANCE = new ClassicSingleton();
}
return UNIQUE_INSTANCE;
}
public String getDescription() {
return "I'm a classic ClassicSingleton!";
}
}
首先来说,为什么它是一个非常适合入门的单例?
因为它确实很简单,这段代码用一段话来描述就是:保证需要使用的对象在内存中的唯一性
个人觉得,这个就是单例的核心思想,后面各种单例模式都是为了这个操作在做各种各样的努力,只是实现的优劣之分而已。
再来聊聊为什么不推荐使用?
因为这个写法是一个线程不安全的,多线程下会有问题。所以这里用词是不推荐,万一你的用法就是单线程呢?这样写也没问题,so,只要能符合业务,通俗易懂,那么它就没有问题。老生常谈的问题,没有最好的,只有最适合的,简单高效才是硬道理,个人认为设计模式也只是为了辅助达成这个目标吧
记得我刚实习的时候,看网上的单例要用volatile, 要用synchronized,看得我那真是一愣一愣的,当时我synchronized,知道是干啥的,但是用得上,volatile,还要靠百度才知道是个啥。本着追求牛X技术的心情,直接就拷了一个个人感觉最牛X的volatile双重判断的写法上去。其实当时并不明白其中原理,只是觉得很牛X而已,幸好没出什么生产环境的bug,也是万幸。
不知道原理的代码是很恐怖的,因为这个东西有些可能是没有通过时间,业务检验的,即便是通过了测试,只要生产环境出问题,那就是毁灭性的(别问我怎么知道的)。技术是为了支撑业务,而不是为了炫技,写出简单,易用,高效的代码才是技术应该做的事情(当然并非是不鼓励尝试新技术,只是需要控制在一个可控的范围内)
打个比方,我写了一个处理权限的功能,其他人需要接进来的时候,我告诉他们,你们要去配一个xml文件,properties文件里面加两个参数,最后使用的时候,要调用xx方法,他们第一感觉就是,你写的这个太难用了。如果你告诉他们,把这个包引进去加个注解就可以了,其他的都不用管呢?是不是感觉完全不一样?
我擦,扯远了,总的来说就是它适合用于学习,不适合用于商业,那么有没有适合用于商业的呢?当然有,网上文章一大堆
第一种,简单粗暴的线程安全
public class ThreadSafeSingleton {
private static ThreadSafeSingleton UNIQUE_INSTANCE;
private ThreadSafeSingleton() {
}
public static synchronized ThreadSafeSingleton getInstance() {
if (UNIQUE_INSTANCE == null) {
UNIQUE_INSTANCE = new ThreadSafeSingleton();
}
return UNIQUE_INSTANCE;
}
public String getDescription() {
return "I'm a thread safe singleton!";
}
}
效率很低,但是能用,依然是不推荐的类型,有的朋友可能要问了,那不推荐你写出来干啥?
它还是有优势的,它理解起来真的很简单,同时编程也不复杂,这个不就是我们一直追寻的东西吗?如果一个问题没有更好的解决方案,那么理解简单,编程简单的方案也不失为一个方案吧?至少能看懂啊
当然单例模式这里确实是不推荐的,因为我知道的还有至少3种比它好,所以不推荐
第二种,使用静态初始化变量
public class StaticallyInitializedSingleton {
private static StaticallyInitializedSingleton UNIQUE_INSTANCE = new StaticallyInitializedSingleton();
private StaticallyInitializedSingleton() {}
public static StaticallyInitializedSingleton getInstance() {
return UNIQUE_INSTANCE;
}
public String getDescription() {
return "I'm a statically initialized ClassicSingleton!";
}
}
这种算是最推荐的写法了,首先它写法简单,其次线程安全问题可以通过jvm去保证(每个类的静态变量在jvm中只会被初始化一次),最后,获取单例类的操作没有加锁处理,性能很高
但是,它也不是没有问题,一般来说,需要单例的类都比较耗性能,创建了不用还是比较伤的(当然,有的时候,有钱能解决很多这样的问题),还有一种可能是如果在初始化类的时候,创建单例类失败了,那这个类里面所有的方法都没法用了,如果在spring的环境下,再有一个@Component之类的注解,或者能够被spring扫描到的其他操作那就更好玩了,可能项目都启动不起来。
这个东西就要看这个单例对于项目是不是强依赖了,仁者见仁智者见智了,此处就不赘述,不然又要跑偏了
第三种,双重加锁检查
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton UNIQUE_INSTANCE;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (UNIQUE_INSTANCE == null) {
synchronized (DoubleCheckSingleton.class) {
if (UNIQUE_INSTANCE == null) {
UNIQUE_INSTANCE = new DoubleCheckSingleton();
}
}
}
return UNIQUE_INSTANCE;
}
public String getDescription() {
return "I'm a double check singleton!";
}
}
这个就是个人觉得一看就是很牛X的那种写法,当然,它的各方面也都是相当优秀的,线程安全,容错,性能都不错
但是,如果对它的理解比较模糊的话,那么写的时候是很容易写掉一些重点的东西的,举两个点:
1,静态变量里面的volatile容易写掉吧?
2,synchronized里面那个判空容易写掉吧?
对于2这点,我是深有体会的,如果A,B线程同时调用getInstance()方法,假设UNIQUE_INSTANCE还没有初始化,同时A线程先进入synchronized块,没有if null判断,那么它就new了一个对象出来吧,当A执行完了以后释放了锁,这个时候B就会进入,没有if null判断,B也new一个对象出来,这就有问题了啊。
对于1这点,如果不理解volatile,是很容易写掉的,毕竟,如果能很好的理解第2点的话,就会感觉,不加这啥volatile感觉也没啥问题啊,双重锁稳的不行啊。但是不加还真有可能有问题
先说说volatile一般的作用:禁止指令重排序,内存可见性
这里的作用是禁止指令重排序,在创建对象并访问的过程中,可以分为4个步骤:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置 instance 指向刚分配的内存地址
instance.invoke() //4:初次访问对象
只要保证在访问对象之前完成1,2,3,对于Java语言规范来说都是允许的,所以这里2和3是可以重排序的,但是多线程的情况下,假如A线程正在进行对象初始化,B线程可能会在第一个if null判断的时候拿到一个不为空,但是还没有初始化完成的对象(2,3被重排序),然后就会出现一些未知的错误
如果使用volatile的话,那么2,3就不会重排序,即使有其他线程拿到对象,也就说明,肯定是已经执行了2,3两步,不然的话if null判断肯定是空
这里是参考了一位大佬的文章:
https://www.infoq.cn/article/double-checked-locking-with-delay-initialization
单例模式的破解与防御
前面大量的篇幅用来说明了怎么在多线程的情况下保证单例模式,这里讲讲怎么从语法层面上来保证。
首先,语法层面上保证单例的一般操作是这样的:
private StaticallyInitializedSingleton() {
}
相当于告诉所有人,这个类只能我自己初始化,你们别搞事情哈,但是它并不是牢不可破的
第一种,使用反射机制
public class SingletonClient {
public static void main(String[] args)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, InterruptedException {
Constructor cons = StaticallyInitializedSingleton.class.getDeclaredConstructor(null);
cons.setAccessible(true);
StaticallyInitializedSingleton singleton = (StaticallyInitializedSingleton)cons.newInstance(null);
System.out.println(singleton.getDescription());
}
}
这种操作怎么防御呢?可以去控制它的类只能初始化一次,具体的操作可以这样:
public class StaticallyInitializedSingleton implements Serializable{
private static StaticallyInitializedSingleton UNIQUE_INSTANCE = new StaticallyInitializedSingleton();
private static boolean INITIALIZED;
private StaticallyInitializedSingleton() {
if(INITIALIZED){
throw new RuntimeException();
}
INITIALIZED = true;
}
public static StaticallyInitializedSingleton getInstance() {
return UNIQUE_INSTANCE;
}
public String getDescription() {
return "I'm a statically initialized ClassicSingleton!";
}
}
在类构造函数中,添加标记INITIALIZED,标明是否已经初始化如果已经初始化,那么就抛出异常。这里因为反射调用的时候,也会先去初始化类,初始化类的时候,就会在静态变量赋值的时候触发创建一次对象,等到反射调用newInstance的时候,就会报错
第二种,使用Java的序列化与反序列化
当然,首先要实现Serializable接口,不然也没有这个问题
public class SerializeTest {
public static void main(String [] args) throws IOException, ClassNotFoundException {
StaticallyInitializedSingleton s = StaticallyInitializedSingleton.getInstance();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File("E:/test.txt")));
objectOutputStream.writeObject(s);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:/test.txt"));
StaticallyInitializedSingleton s1 = (StaticallyInitializedSingleton)ois.readObject();
System.out.println(s);
System.out.println(s1);
}
}
执行结果就会发现s,s1不是一致的,解决这种情况,需要在单例类中加入readResolve()方法来控制,JVM在反序列化的时候,使用我们自定义的类作为结果
public class StaticallyInitializedSingleton implements Serializable {
private static StaticallyInitializedSingleton UNIQUE_INSTANCE = new StaticallyInitializedSingleton();
private static boolean INITIALIZED;
private StaticallyInitializedSingleton() {
if(INITIALIZED){
throw new RuntimeException();
}
INITIALIZED = true;
}
public static StaticallyInitializedSingleton getInstance() {
return UNIQUE_INSTANCE;
}
public String getDescription() {
return "I'm a statically initialized ClassicSingleton!";
}
//解决序列化与反序列化问题
private Object readResolve(){
return uniqueInstance;
}
}
花了这么多功夫,终于解决了,那么实际开发中,有没有必要这样去处理这些问题呢?
这个我给不出答案,可以给出的参考有两个:1,编程成本;2,程序边界
通俗点讲就是:1,改这个要花多久时间(编写,测试,上线);2,不按规范来调用,是不是程序需要关注地方
当然,有没有一劳永逸的方法来解决各种各样问题,并且编写简单,容易理解呢?
请看:
public enum Singleton {
INSTANCE;
public String getDescription() {
return "枚举单例,就是这么简单";
}
}
没错,枚举类,是不是感觉比上面所有都简单;)
headfirst设计模式(6)—单例模式的更多相关文章
- HeadFirst设计模式之单例模式
一. 1.The Singleton Pattern ensures a class has only one instance, and provides a global point of acc ...
- Delphi 设计模式:《HeadFirst设计模式》Delphi2007代码---单例模式之ChocolateBoiler[转]
1 2{<HeadFirst设计模式>之单例模式 } 3{ 编译工具: Delphi2007 for win32 } 4{ E-Mail : guzh-0417@163.com ...
- headfirst设计模式(5)—工厂模式体系分析及抽象工厂模式
先编一个这么久不写的理由 上周我终于鼓起勇气翻开了headfirst设计模式这本书,看看自己下一个设计模式要写个啥,然后,我终于知道我为啥这么久都没写设计模式了,headfirst的这个抽象工厂模式, ...
- 设计模式之单例模式(Singleton)
设计模式之单例模式(Singleton) 设计模式是前辈的一些经验总结之后的精髓,学习设计模式可以针对不同的问题给出更加优雅的解答 单例模式可分为俩种:懒汉模式和饿汉模式.俩种模式分别有不同的优势和缺 ...
- GJM : C#设计模式(1)——单例模式
感谢您的阅读.喜欢的.有用的就请大哥大嫂们高抬贵手"推荐一下"吧!你的精神支持是博主强大的写作动力以及转载收藏动力.欢迎转载! 版权声明:本文原创发表于 [请点击连接前往] ,未经 ...
- java设计模式之单例模式(几种写法及比较)
概念: Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建 ...
- 每天一个设计模式-4 单例模式(Singleton)
每天一个设计模式-4 单例模式(Singleton) 1.实际生活的例子 有一天,你的自行车的某个螺丝钉松了,修车铺离你家比较远,而附近的五金店有卖扳手:因此,你决定去五金店买一个扳手,自己把螺丝钉固 ...
- 设计模式之单例模式的简单demo
/* * 设计模式之单例模式的简单demo */ class Single { /* * 创建一个本类对象. * 和get/set方法思想一样,类不能直接调用对象 * 所以用private限制权限 * ...
- 设计模式之单例模式——Singleton
设计模式之单例模式--Singleton 设计意图: 保证类仅有一个实例,并且可以供应用程序全局使用.为了保证这一点,就需要这个类自己创建自己的对象,并且对外有 ...
- 10月27日PHP加载类、设计模式(单例模式和工厂模式)、面向对象的六大原则
加载类可以使用include.require.require_once三种中的任意一种,每个关键字都有两种方法,但是这种方法的缺点是需要加载多少个php文件,就要写多少个加载类的方法.一般也就需要加载 ...
随机推荐
- Robot Framework 源码解析(1) - java入口点
一直很好奇Robot Framework 是如何通过关键字驱动进行测试的,好奇它是如何支持那么多库的,好奇它是如何完成截图的.所以就打算研究一下它的源码. 这是官方给出的Robot framework ...
- JAVA线程及简单同步实现的原理解析
线程 一.内容简介: 本文主要讲述计算机中有关线程的相关内容,以及JAVA中关于线程的基础知识点,为以后的深入学习做铺垫.如果你已经是高手了,那么这篇文章并不适合你. 二.随笔正文: 1.计算机系统组 ...
- Actor模型-Akka
英文原文链接,译文链接,原文作者:Arun Manivannan ,译者:有孚 写过多线程的人都不会否认,多线程应用的维护是件多么困难和痛苦的事.我说的是维护,这是因为开始的时候还很简单,一旦你看到性 ...
- LVM 移除PV步骤
1.先查看需要收缩文件系统的使用情况,收缩后的文件系统空间不能小于已经使用的空间 df -hT 2.卸载需要收缩的文件系统(以/dev/vg0/lvm1为例) umount /dev/vg0/lvm1 ...
- 这可能是史上最好的 Java8 新特性 Stream 流教程
本文翻译自 https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ 作者: @Winterbe 欢迎关注个人微信公众 ...
- 利用ATiny85制作BadUSB
0x00.准备: ATiny85的板子 淘宝十元包邮.有两款,两款都可以,建议选择左边的,这样可以直接插入USB口,第二款也可以,不过需要一根Micro的数据线(旧款安卓手机使用的线). 电脑安装驱动 ...
- formidable处理node.js的post请求
前言 我们都知道在node.js中,我们最常用的请求方式是get和post.其中get请求和URL相关,通过解析URL我们可以直接获取到请求的参数.但是post请求不同,post请求是包含在请求体中, ...
- 关于MQ,你必须知道的
我走过最长的路是你的套路 女:二号男嘉宾,假如我们牵手成功后,你会买名牌包包给我吗? 男:那你会听话吗? 女:会 听话. 男:听话 咱不买! OK那么消息队列MQ有什么套路呢?(这个话题转换生硬度连我 ...
- netcore服务程序暴力退出导致的业务数据不一致的一种解决方案(优雅退出)
一: 问题提出 现如今大家写的netcore程序大多部署在linux平台上,而且服务程序里面可能会做各种复杂的操作,涉及到多数据源(mysql,redis,kafka).成功部署成后台 进程之后,你以 ...
- bat脚本自定义魔兽warIII运行分辨率,去黑边
我们一般平时安装完WarIII后运行时的分辨率默认是800*600,导致有黑边的存在.所以我写了一个bat脚本来自定义WarIII的运行分辨率.需要以管理员身份运行. 下载链接: 链接:https:/ ...