文档章节

《设计模式》之单例模式

h
 haidao1992
发布于 2017/08/06 13:23
字数 2158
阅读 7
收藏 0

        首先要解释一下什么是延迟加载,延迟加载就是等到真真使用的时候才去创建实例,不用时不要去创建。

        从速度和反应时间角度来讲,非延迟加载(又称饿汉式)好;从资源利用效率上说,延迟加载(又称懒汉式)好。

下面看看几种常见的单例的设计方式:

第一种:非延迟加载单例类

Java代码 

  1. public class Singleton {  
  2.  private Singleton() {}  
  3.  private static final Singleton instance = new Singleton();  
  4.  public static Singleton getInstance() {  
  5.   return instance;  
  6.  }  
  7. }  

第二种:同步延迟加载

Java代码 

  1. public class Singleton {  
  2.  private static Singleton instance = null;  
  3.  private Singleton() {}  
  4.  public static synchronized Singleton getInstance() {  
  5.   if (instance == null) {  
  6.    instance = new Singleton();  
  7.   }  
  8.   return instance;  
  9.  }  
  10. }  

第三种:双重检测同步延迟加载 
        为处理原版非延迟加载方式瓶颈问题,我们需要对 instance 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同获取锁了),但在Java中行不通,因为同步块外面的if (instance == null)可能看到已存在,但不完整的实例。JDK5.0以后版本若instance为volatile则可行:

Java代码 

  1. public class Singleton {  
  2.  private volatile static Singleton instance = null;  
  3.  private Singleton() {}  
  4.  public static Singleton getInstance() {  
  5.   if (instance == null) {  
  6.    synchronized (Singleton.class) {// 1  
  7.     if (instance == null) {// 2  
  8.      instance = new Singleton();// 3  
  9.     }  
  10.    }  
  11.   }  
  12.   return instance;  
  13.  }  
  14. }  

        双重检测锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是失败的一个主要原因。

无序写入
        为解释该问题,需要重新考察上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。这行代码的问题是:在 Singleton 构造函数体执行之前,变量 instance 可能成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程得到的是一个还会初始化的对象,这样会导致系统崩溃。
        什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设代码执行以下事件序列:
1、线程 1 进入 getInstance() 方法。
2、由于 instance 为 null,线程 1 在 //1 处进入 synchronized 块。 
3、线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null。 
4、线程 1 被线程 2 预占。
5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 instance 引用返回给一个构造完整但部分初始化了的 Singleton 对象。 
6、线程 2 被线程 1 预占。
7、线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。

        为展示此事件的发生情况,假设代码行 instance =new Singleton(); 执行了下列伪代码:
mem = allocate();             //为单例对象分配内存空间.
instance = mem;               //注意,instance 引用现在是非空,但还未初始化
ctorSingleton(instance);    //为单例对象通过instance调用构造函数
这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。

如果真像这篇文章:http://dev.csdn.net/author/axman/4c46d233b388419e9d8b025a3c507b17.html所说那样的话,1.2或以后的版本就不会有问题了,但这个规则是JMM的规范吗?谁能够确认一下。
确实,在JAVA2(以jdk1.2开始)以前对于实例字段是直接在主储区读写的.所以当一个线程对resource进行分配空间,
初始化和调用构造方法时,可能在其它线程中分配空间动作可见了,而初始化和调用构造方法还没有完成.

但是从JAVA2以后,JMM发生了根本的改变,分配空间,初始化,调用构造方法只会在线程的工作存储区完成,在没有
向主存储区复制赋值时,其它线程绝对不可能见到这个过程.
而这个字段复制到主存区的过程,更不会有分配空间后
没有初始化或没有调用构造方法的可能.在JAVA中,一切都是按引用的值复制的.向主存储区同步其实就是把线程工作
存储区的这个已经构造好的对象有压缩堆地址值COPY给主存储区的那个变量.这个过程对于其它线程,要么是resource
为null,要么是完整的对象.绝对不会把一个已经分配空间却没有构造好的对象让其它线程可见.

另一篇详细分析文章:http://www.iteye.com/topic/260515

第四种:使用ThreadLocal修复双重检测

借助于ThreadLocal,将临界资源(需要同步的资源)线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为了线程局部范围内来作。这里的ThreadLocal也只是用作标示而已,用来标示每个线程是否已访问过,如果访问过,则不再需要走同步块,这样就提高了一定的效率。但是ThreadLocal在1.4以前的版本都较慢,但这与volatile相比却是安全的。

Java代码 

  1. public class Singleton {  
  2.  private static final ThreadLocal perThreadInstance = new ThreadLocal();  
  3.  private static Singleton singleton ;  
  4.  private Singleton() {}  
  5.    
  6.  public static Singleton  getInstance() {  
  7.   if (perThreadInstance.get() == null){  
  8.    // 每个线程第一次都会调用  
  9.    createInstance();  
  10.   }  
  11.   return singleton;  
  12.  }  
  13.   
  14.  private static  final void createInstance() {  
  15.   synchronized (Singleton.class) {  
  16.    if (singleton == null){  
  17.     singleton = new Singleton();  
  18.    }  
  19.   }  
  20.   perThreadInstance.set(perThreadInstance);  
  21.  }  
  22. }  

第五种:使用内部类实现延迟加载
为了做到真真的延迟加载,双重检测在Java中是行不通的,所以只能借助于另一类的类加载加延迟加载:

Java代码 

  1. public class Singleton {  
  2.  private Singleton() {}  
  3.  public static class Holder {  
  4.   // 这里的私有没有什么意义  
  5.   /* private */static Singleton instance = new Singleton();  
  6.  }  
  7.  public static Singleton getInstance() {  
  8.   // 外围类能直接访问内部类(不管是否是静态的)的私有变量  
  9.   return Holder.instance;  
  10.  }  
  11. }  

单例测试

下面是测试单例的框架,采用了类加载器与反射。
注,为了测试单便是否为真真的单例,我自己写了一个类加载器,且其父加载器设置为根加载器,这样确保Singleton由MyClassLoader加载,如果不设置为根加载器为父加载器,则默认为系统加载器,则Singleton会由系统加载器去加载,但这样我们无法卸载类加载器,如果加载Singleton的类加载器卸载不掉的话,那么第二次就不能重新加载Singleton的Class了,这样Class不能得加载则最终导致Singleton类中的静态变量重新初始化,这样就无法测试了。
下面测试类延迟加载的结果是可行的,同样也可用于其他单例的测试:

Java代码 

  1. public class Singleton {  
  2.  private Singleton() {}  
  3.   
  4.  public static class Holder {  
  5.   // 这里的私有没有什么意义  
  6.   /* private */static Singleton instance = new Singleton();  
  7.  }  
  8.   
  9.  public static Singleton getInstance() {  
  10.   // 外围类能直接访问内部类(不管是否是静态的)的私有变量  
  11.   return Holder.instance;  
  12.  }  
  13. }  
  14.   
  15. class CreateThread extends Thread {  
  16.  Object singleton;  
  17.  ClassLoader cl;  
  18.   
  19.  public CreateThread(ClassLoader cl) {  
  20.   this.cl = cl;  
  21.  }  
  22.   
  23.  public void run() {  
  24.   Class c;  
  25.   try {  
  26.    c = cl.loadClass("Singleton");  
  27.    // 当两个不同命名空间内的类相互不可见时,可采用反射机制来访问对方实例的属性和方法  
  28.    Method m = c.getMethod("getInstance", new Class[] {});  
  29.    // 调用静态方法时,传递的第一个参数为class对象  
  30.    singleton = m.invoke(c, new Object[] {});  
  31.    c = null;  
  32.    cl = null;  
  33.   } catch (Exception e) {  
  34.    e.printStackTrace();  
  35.   }  
  36.  }  
  37. }  
  38.   
  39. class MyClassLoader extends ClassLoader {  
  40.  private String loadPath;  
  41.  MyClassLoader(ClassLoader cl) {  
  42.   super(cl);  
  43.  }  
  44.  public void setPath(String path) {  
  45.   this.loadPath = path;  
  46.  }  
  47.  protected Class findClass(String className) throws ClassNotFoundException {  
  48.   FileInputStream fis = null;  
  49.   byte[] data = null;  
  50.   ByteArrayOutputStream baos = null;  
  51.   
  52.   try {  
  53.    fis = new FileInputStream(new File(loadPath  
  54.      + className.replaceAll("\\.", "\\\\") + ".class"));  
  55.    baos = new ByteArrayOutputStream();  
  56.    int tmpByte = 0;  
  57.    while ((tmpByte = fis.read()) != -1) {  
  58.     baos.write(tmpByte);  
  59.    }  
  60.    data = baos.toByteArray();  
  61.   } catch (IOException e) {  
  62.    throw new ClassNotFoundException("class is not found:" + className,  
  63.      e);  
  64.   } finally {  
  65.    try {  
  66.     if (fis != null) {  
  67.      fis.close();  
  68.     }  
  69.     if (fis != null) {  
  70.      baos.close();  
  71.     }  
  72.   
  73.    } catch (Exception e) {  
  74.     e.printStackTrace();  
  75.    }  
  76.   }  
  77.   return defineClass(className, data, 0, data.length);  
  78.  }  
  79. }  
  80.   
  81. class SingleTest {  
  82.  public static void main(String[] args) throws Exception {  
  83.   while (true) {  
  84.    // 不能让系统加载器直接或间接的成为父加载器  
  85.    MyClassLoader loader = new MyClassLoader(null);  
  86.    loader  
  87.      .setPath("D:\\HW\\XCALLC16B125SPC003_js\\uniportal\\service\\AAA\\bin\\");  
  88.    CreateThread ct1 = new CreateThread(loader);  
  89.    CreateThread ct2 = new CreateThread(loader);  
  90.    ct1.start();  
  91.    ct2.start();  
  92.    ct1.join();  
  93.    ct2.join();  
  94.    if (ct1.singleton != ct2.singleton) {  
  95.     System.out.println(ct1.singleton + " " + ct2.singleton);  
  96.    }  
  97.    // System.out.println(ct1.singleton + " " + ct2.singleton);  
  98.    ct1.singleton = null;  
  99.    ct2.singleton = null;  
  100.    Thread.yield();  
  101.   }  
  102.  }  
  103. }  

© 著作权归作者所有

h
粉丝 1
博文 26
码字总数 27021
作品 0
浦东
程序员
私信 提问
【设计模式笔记】(十六)- 代理模式

一、简述 代理模式(Proxy Pattern),为其他对象提供一个代理,并由代理对象控制原有对象的引用;也称为委托模式。 其实代理模式无论是在日常开发还是设计模式中,基本随处可见,中介者模式中...

MrTrying
2018/06/24
0
0
设计模式已经陨落了?

你现在是坐在一个程序员旁边吗?如果是的话,那么在你读下面的段落之前,有一个简单的实验。让他们到一边去,问问他们两个问题并记录下答案。首先问他们“什么是设计模式?”然后再问“说出你...

oschina
2014/03/11
9.4K
69
《PHP设计模式大全》系列分享专栏

《PHP设计模式大全》已整理成PDF文档,点击可直接下载至本地查阅 https://www.webfalse.com/read/201739.html 文章 php设计模式介绍之编程惯用法第1/3页 php设计模式介绍之值对象模式第1/5页...

kaixin_code
2018/11/06
170
0
设计模式梳理(一)

设计模式梳理(一) 总体来说设计模式分为三大类: @案例源码地址:https://gitlab.com/lxqxsyu/DisgnPattern 创建型模式 简单工厂模式 工厂类是整个模式的关键。它包含必要的判断逻辑,能够...

lxq_xsyu
2017/11/02
0
0
JavaScript 的一些设计模式

设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案 设计模式是前人解决某个特定场景下对而总结出来的一些解决方案。可能刚开始接触编程还没有什么经验的时候,会...

格西南
08/20
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring Boot + Mybatis-Plus 集成与使用(二)

前言: 本章节介绍MyBatis-Puls的CRUD使用。在开始之前,先简单讲解下上章节关于Spring Boot是如何自动配置MyBatis-Plus。 一、自动配置 当Spring Boot应用从主方法main()启动后,首先加载S...

伴学编程
昨天
7
0
用最通俗的方法讲spring [一] ──── AOP

@[TOC](用最通俗的方法讲spring [一] ──── AOP) 写这个系列的目的(可以跳过不看) 自己写这个系列的目的,是因为自己是个比较笨的人,我曾一度怀疑自己的智商不适合干编程这个行业.因为在我...

小贼贼子
昨天
7
0
Flutter系列之在 macOS 上安装和配置 Flutter 开发环境

本文为Flutter开发环境在macOS下安装全过程: 一、系统配置要求 想要安装并运行 Flutter,你的开发环境需要最低满足以下要求: 操作系统:macOS(64位) 磁盘空间:700 MB(不包含 IDE 或其余...

過愙
昨天
6
0
OSChina 周六乱弹 —— 早上儿子问我他是怎么来的

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @凉小生 :#今日歌曲推荐# 少点戾气,愿你和这个世界温柔以待。中岛美嘉的单曲《僕が死のうと思ったのは (曾经我也想过一了百了)》 《僕が死の...

小小编辑
昨天
2.6K
16
Excption与Error包结构,OOM 你遇到过哪些情况,SOF 你遇到过哪些情况

Throwable 是 Java 中所有错误与异常的超类,Throwable 包含两个子类,Error 与 Exception 。用于指示发生了异常情况。 Java 抛出的 Throwable 可以分成三种类型。 被检查异常(checked Exc...

Garphy
昨天
42
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部