文档章节

Java设计模式 - 单例模式(末尾有彩蛋😄)

那只是一股逆流
 那只是一股逆流
发布于 2016/11/24 09:21
字数 2085
阅读 16
收藏 0
点赞 0
评论 0

 

定义

  确保只有一个类只有一个实例,并提供全局访问点。

为什么要使用它

  对一些类来说保证只有一个实例是很重要的。比如windows操作系统中的资源管理器,回收站等工具必须保证只有一个实例,否则系统将会出现一些意想不到的异常。

优点

  因为只有一个实例,所以很容易控制它的访问权限;避免了过多的使用静态变量等。

适用范围

  当类只能有一个实例而且客户可以从一个众所周知的访问点访问它。

结构(UML)

  单例模式的结构比较简单,只有一个类:它的构造器是私有的并且提供了一个返回一个实例的静态方法。

  单例模式的UML比较简单:

  

实现

  假设现在你有一个地方需要使用到单例模式,你可能首先会想到这样写:

package com.tony.singleton;
/**
 * 1、私有化构造器  
 * 2、提供一个返回实例的方法(全局访问点)  
 * 这种方法叫做懒汉式  
 */
public class Singleton01 {  
     private static Singleton01 instance = null; 
     //私有化构造器
     private Singleton01(){ 
     } 
     //静态工厂方法
     public static Singleton01 getInstance(){ 
         if(instance == null){ 
             instance = new Singleton01(); 
         } 
         return instance; 
    } 
}


  但是这种方法又有一个问题:现在的程序一般都是多线程的,在并发的情况下可能会出现两个实例!

  让我们来分析一下这种情况:现在有两个线程(A、B)同时调用了getInstance()方法,然后一个A线程发现instance为null。当A线程正准备去new一个实例时,B线程也发现instance为null,所以B线程也去new一个实例。这种情况下就会出现两个实例。

  怎么解决呢?

  此时我们会想这还不简单,给它加一个synchronized同步锁不就OK了?!

package com.tony.singleton;  
 
/**
 * 1、私有化构造器  
 * 2、提供一个返回实例的方法(全局访问点)  
 */
public class Singleton02 {  
    private static Singleton02 instance = null;  
    //私有化构造器
    private Singleton02(){ 
    } 
    //静态工厂方法
    public static synchronized Singleton02 getInstance(){ 
         if(instance == null){ 
             instance = new Singleton02(); 
         } 
         return instance; 
    } 
}
 

  OK问题解决了!同步这种方法简单易行解决了线程的并发问题,但同时又带来了一个新的问题:对性能的影响。如果getInstance()频繁被调用,那么你就得重新考虑了:每次调用之前都要给它加同步锁!你要知道同步一个方法可能造成程序执行效率下降100倍,而且只有实例化这个对象时才需要同步。

  好吧,既然麻烦都在实例化对象这里,那么我在类加载器加载的时候我就把这个对象实例化了是不是就可以呢?

package com.tony.singleton;  
 
/**
 *  
 * 这种方式就做 饿汉式  
 */
public class Singleton03 {  
    //当被类加载器加载的时候就把这个类给实例化
     private static Singleton03 instance = new Singleton03(); 
     //私有化构造器
     private Singleton03(){ 
     } 
     //静态工厂方法
     public static Singleton03 getInstance(){ 
         return instance; 
     }
}
 

  利用这种做法,我们依赖JVM在加载这个类时马上创建此唯一的实例。JVM保证在任何线程访问instance变量之前,一定先创建此实例。

  这种做法很好,基本上解决了上面的出现的两个问题:1、并发访问;2、对性能的影响。

  这种做法适合不太复杂的实例。如果需要实例化的的类很复杂,在创建和运行时方面的负担太重就会增加JVM的负担。

  有没有这样的方法:除了解决最初的那两个问题外还能延时加载,减轻JVM的负担?答案是有!

  这种方法叫做双重检测锁。这次引进了一个新东西:volatile 关键字。

 package com.tony.singleton;
 
/**
 * 双重检测锁  
 *
 */
public class Singleton04 {  
    //增加volatile关键字!!!
    private volatile static Singleton04 instance = null; 
    //私有化构造器
    private Singleton04(){ 
    } 
    //静态工厂方法
    public static synchronized Singleton04 getInstance(){ 
         if(instance == null){ 
            //这段代码仅有一次执行的机会:只有第一次才彻底执行
            synchronized(Singleton04.class){ 
                 if(instance == null){ 
                     instance = new Singleton04(); 
                 } 
            }
         } 
         return instance; 
   } 
}
 1

  这时候synchronized所同步的那段代码只会执行一次,而且还保证了延时加载。

总结

  实现单例模式的方法还有几种,但比较常用的就是这几种。一般掌握饿汉式、懒汉式和双重检测锁就可以了。

  这几种各有优缺点,具体使用那种还有具体情况具体分析。

  懒汉式:能够延时加载,但可能对性能影响较大。

  懒汉式:对性能影响较小,但不能延时加载。

  双重检测锁:能够延时加载,只需同步一次,但是不支持JDK1.4之前的版本。

-----------------------分割线----11.24-------------------------

    现在看着一年前自己写的博客,感觉好low~~~

    现在有些问题之前没有考虑到的,现在提出来:

  1. 双重检测锁为什么要加上volatile关键字?如果没有会有什么影响?
  2. 双重检测锁真的比其他的单例实现更好吗?

    针对上述的问题,我来一一分析回答。

  1.     针对第一个问题,其实当时写博客的时候我也不知道为什么要加volatile关键字,因为书上是这么写的,😄。其实这里涉及到一个重排序的问题。什么是重排序?Java虚拟机在执行字节码的时候为了使代码运行时获得更好的性能和效率有权利对字节码进行语义上的重新调整,只要不影响最后的结果即可。下面我举一个例子:
    private Object a = new Object();

    这是一行再普通不过的代码了,按照我们的经验是从右往左执行的:首先(a)在堆中分配一个Object内存大小的空间,(b)初始化Object实例,然后(c)将引用赋值给a。嗯,没错,一般情况下是这样的。但是在多线程的情况下就不一样了,代码有可能是这样执行的:(a)在堆中分配一个Object内存大小的空间,(c)将引用赋值给a,(b)初始化Object实例。第一步还是一样:分配堆内存,第二步第三步交换了一下顺序。咋一看好像没啥影响啊,但是假如有两条线程(A、B)交替执行就会出问题。继续拿双重检测锁的代码用用,假如A线程首先执行,直到

    instance = new Singleton04();

    这一行代码,此时发生上述的重排序问题,instance获得引用,但此时内存中实例初始化还没有初始化,现在CPU把A线程切换出去。把B线程切换进来,然后执行这段代码,因为instance变量已经获得了内存的引用条件instance == null 都为false,直到return instance。当其他代码拿到实例,然后执行其方法时,GG,NullPointerException。如果能够理解我刚刚的解释,那么为什么要加volatile关键字也就不难理解了。volatile修饰的变量可以保证任何一个线程对其的改动都会被后面的线程所察觉(happens-before,具体信息参看这里)。

  2. 针对第二个问题,我主要想表达随着计算机性能的提高,延迟初始化的优点已经显得微不足道。除非这个类真的非常复杂,加载时需要非常多的资源。如果真的是这样,那么我们可能就要反思一下我们的设计是不是出了问题,一个类不能让它承担太多的职责,否则后面维护起来非常麻烦。而且这样也不利用团队协作~所以就现在的计算能力完全不需要延迟初始化,通过使用static关键字,或者使用枚举类不失为一种好的单例实现方式。

好了,如果大家还有什么疑问欢迎留言,我会尽可能的回答大家的问题~

  

相关知识点

  • happens-before
  • 重排序
  • volatile关键字

参考资料

  《Head First 设计模式》

      《Java 并发编程实战》

  《设计模式》

 

© 著作权归作者所有

共有 人打赏支持
那只是一股逆流
粉丝 9
博文 22
码字总数 26214
作品 0
南岸
后端工程师
JavaScript设计模式系列三之单例模式(附案例源码)

文章初衷 设计模式其实旨在解决语言本身存在的缺陷 目前javaScript一些新的语法特性已经集成了一些设计模式的实现, 大家在写代码的时候,没必要为了用设计模式而去用设计模式, 那么我这边为什...

小钱钱阿圣
2017/09/22
0
0
20个设计模式和软件设计面试问题

不管是参加Java面试还是C#面试,设计模式和软件设计都是任何编程面试中的必问问题。实际上,编程能力和设计技巧是对彼此很好的补充。一个好的程序员通常都是一个好的软件设计人员。他们知道怎...

LCZ777
2014/08/13
0
0
设计模式 2014-12-19

book: 阎宏《JAVA与模式》 架构设计栏目 http://blog.csdn.net/enterprise/column.html 概要: http://bbs.csdn.net/forums/Embeddeddriver 23种设计模式分别是: 1.单例模式 2.工厂方法模式...

jayronwang
2014/12/19
0
0
设计模式15——Template Method设计模式

Template Method模板方法设计模式定义一个操作中算法的骨架,将具体步骤的执行延迟到子类中实现。Java中的抽象类就是使用了模板方法设计模式。模板方法设计模式结构如下: 以文档处理为例,T...

小米米儿小
2014/01/24
0
0
设计模式3——Proxy设计模式

Proxy代理设计模式是一种控制对象访问的设计模式,类似于网络代理,网络代理机制如下图: Proxy代理设计模式机制如下: 代理模式UML图如下: 代理模式顺序图如下: 客户端程序通过代理程序来...

小米米儿小
2013/12/06
0
0
简单工厂、工厂方法、抽象工厂、策略模式、策略与工厂的区别

转载:原地址http://www.cnblogs.com/zhangchenliang/p/3700820.html 简单工厂、工厂方法、抽象工厂、策略模式、策略与工厂的区别 结合简单示例和UML图,讲解工厂模式简单原理。 一、引子 话说...

法斗斗
05/08
0
0
Java设计模式 create FactoryMethod

工厂模式是我们最常用的模式了,著名的Jive论坛,就大量使用了工厂模式,工厂模式在Java程序系统可以说是随处可见。 为什么工厂模式是如此常用?因为工厂模式就相当于创建实例对象的new,我们经...

神勇小白鼠
2011/05/18
0
0
23种设计模式(1):单例模式

定义:确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。 类型:创建类模式 类图: 类图知识点: 1.类图分为三部分,依次是类名、属性、方法 2.以<<开头和以>>结尾的为注释...

LCZ777
2014/07/05
0
0
groovy 闭包【命令模式】

在Java编程世界,有两个设计模式又简单、又常用,而且实现方式也很相似,这就是命令模式和策略模式。 有关这两个模式的讲解,在网络上真是汗牛充栋,大家可以随便搜索一下,就是一大堆。 可以...

k_k_anna
2015/01/20
0
0
为什么java中用枚举实现单例模式会更好

枚举单例是java中使用枚举提供一个实例对象来实现单例模式的一种新方法,虽然单例模式在java中早已存在,但枚举单例实际上从java5引入枚举作为它的关键特性之后相对来说还是一个新的概念,这...

zhoujy
2013/06/01
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

pbgo: 基于Protobuf的迷你RPC/REST框架

https://www.oschina.net/p/pbgo

chai2010
23分钟前
0
0
rsync工具介绍、常用选项以及通过ssh同步

linux下的文件同步工具 rsync rsync是非常实用的一个同步工具,可以从a机器到b机器传输一个文件,也可以备份数据,系统默认没有这个工具,要使用命令 yum install -y rsync 安装。 rsync的命...

黄昏残影
39分钟前
0
0
OSChina 周四乱弹 —— 表妹要嫁人 舅妈叮嘱……

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @哈哈哈哈哈嗝:一定要听——The Pancakes的单曲《咁咁咁》 《咁咁咁》- The Pancakes 手机党少年们想听歌,请使劲儿戳(这里) @clouddyy :...

小小编辑
今天
145
4
流利阅读笔记30-20180719待学习

重磅:让人类得老年痴呆的竟是它? Lala 2018-07-19 1.今日导读 去年奥斯卡最佳动画长片《寻梦环游记》里有一句经典台词:“比死亡更可怕的,是遗忘”。在电影中,年迈的曾祖母会重复说一样的...

aibinxiao
今天
3
0
1.16 Linux机器相互登录

Linux机器之间以密码方式互相登录 运行命令#ssh [ip address],标准命令:#ssh [username]@ip, 如果没有写用户名,则默认为系统当前登录的用户 命令#w查看系统负载,可查看到连接到该主机的...

小丑鱼00
今天
0
0
about git flow

  昨天元芳做了git分支管理规范的分享,为了拓展大家关于git分支的认知,这里我特意再分享这两个关于git flow的链接,大家可以看一下。 Git 工作流程 Git分支管理策略   git flow本质上是...

qwfys
今天
2
0
Linux系统日志文件

/var/log/messages linux系统总日志 /etc/logrotate.conf 日志切割配置文件 参考https://my.oschina.net/u/2000675/blog/908189 dmesg命令 dmesg’命令显示linux内核的环形缓冲区信息,我们可...

chencheng-linux
今天
1
0
MacOS下给树莓派安装Raspbian系统

下载镜像 前往 树莓派官网 下载镜像。 点击 最新版Raspbian 下载最新版镜像。 下载后请,通过 访达 双击解压,或通过 unzip 命令解压。 检查下载的文件 ls -lh -rw-r--r-- 1 dingdayu s...

dingdayu
今天
1
0
spring boot使用通用mapper(tk.mapper) ,id自增和回显等问题

最近项目使用到tk.mapper设置id自增,数据库是mysql。在使用通用mapper主键生成过程中有一些问题,在总结一下。 1、UUID生成方式-字符串主键 在主键上增加注解 @Id @GeneratedValue...

北岩
今天
2
0
告警系统邮件引擎、运行告警系统

告警系统邮件引擎 cd mail vim mail.py #!/usr/bin/env python#-*- coding: UTF-8 -*-import os,sysreload(sys)sys.setdefaultencoding('utf8')import getoptimport smtplibfr......

Zhouliang6
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部