定义:

  保证一个类仅有一个实例,并提供一个全局访问点

适用场景:

  确保任何情况下这个对象只有一个实例

详解:

  1. 私有构造器
  2. 单利模式中的线程安全+延时加载
  3. 序列化和反序列化安全,
  4. 防止反射攻击
  5. 结合JDK源码分析设计模式

1.私有构造器:
  将本类的构造器私有化,其实这是单例的一个非常重要的步骤,没有这个步骤,可以说你的就不是单例模式。这个步骤其实是防止外部函数在new的时候能构造出来新的对象,我们说单例要保证一个类只有一个实例,如果外部能new新的对象,那我们单例就是失败的。所以无论什么时候一定要将这个构造器私有化

2.单例模式中的线程安全+延时加载(懒汉式):

  其实从单线程角度来看,懒汉式是安全。这里我们先来介绍一个线程安全的懒汉式接下来我们从三个版本的懒汉式来分析如何即做到线程安全又做到效率提高

  2.1原始版本

public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton(){
if(lazySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}

  我们来稍微分析一下为什么线程不安全,现在有A,B两个线程,假设两个线程同时都走到了lazySingleton = new LazySingleton();这个创建对象的行,当都执行完的时候,就会创建两个不同的对象然后分别返回。所以违背了单例模式的定义

  2.2加锁

  可能很多人会直接在getInstance()方法上加一个synchronize关键字,这样做完全可以但是效率会较慢,因为synchronize相当于锁了整个对象,下面的双锁结构就会比较轻量级一点

public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
private LazyDoubleCheckSingleton(){ }
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}

  可能很多人一眼就看见synchronize关键字位置变换了,锁的范围变小了,但是最关键的一个是private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;中的volatile关键字,因为如果不加这个关键字的时候,JVM会对没有依赖关系的语句进行重排序,就是可能会在线程A的时候底层先设置lazyDoubleCheckSingleton 指向刚分配的内存地址,然后再来初始化对象,线程B呢在线程A设置lazyDoubleCheckSingleton 指向刚分配的内存地址完后就走到了第一个if,这时判断是不为空的所以都没有竞争synchronize中的资源就直接返回了,但是注意线程A并没有初始化完对象,所以这时就会出错。为了解决上述问题,我们可以引入volatile关键字,这个关键字是会有读屏障写屏障的,也就是由这个关键字修饰的变量,它中间的操作会额外加一层屏障来隔绝,详情可以参考这篇博客。就会禁止一些操作的重排序。
  2.3静态内部类

public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){
if(InnerClass.staticInnerClassSingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
} }

  我在类内部直接定义一个静态内部类,在这个类需要加载的时候我直接把初始化的工作放在了静态内部类中,当有几个线程进来的时候,在class加载后被线程使用之前都是类的初始化阶段,在这个阶段JVM会获取一个锁,这个锁可以同步多个线程对一个类的初始化,然后在内部类的初始化中会进行StaticInnerClassSingleton类的初始化。可以这么理解,其实我们这个也是加了锁,不过这是JVM内部加的锁。

3.序列化与反序列化安全

  下面先介绍一下饿汉式

public class HungrySingleton implements Serializable{

private final static HungrySingleton hungrySingleton;

static{
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
} private Object readResolve(){
return hungrySingleton;
} }

  饿汉式就是在类的初始化阶段就已经加载好了,就算你不用这个对象,这个对象也已经创建好,不像懒汉式要等到要用的时候才加载。这是两种模式的一个很大的区别,事实上饿汉式是线程安全的,就像懒汉式的内部类加载一样,是由JVM加的锁,但是两者都不一定是序列化安全的。
  上面的饿汉式是序列化安全的,为什么?因为多加了readResolve()方法。这时候有人会问为什么要在饿汉式上多加一个这个方法。这里的源码我就不一一解析了。事实上在反序列化(从文件中读取类)的时候,底层会有一个判断。如果这个类在运行时是可序列化的,那么我就会在读取的时候创建一个新的类(反射创建),否则我就会让这个类为空。再后面又有一个判断,如果我的类这时候不为空,我就会通过反射尝试调用readResolve()方法,然后最终返回给我的ObjectInputStream流。没有的话我就返回之前创建的新对象。所以这就相当于覆盖了之前读取时候创建的类

4.防止反射攻击

  看完上面的代码你会发现,我基本上都在私有构造器中加入一个空判断来抛出异常,反射攻击的时候,上面的懒汉式中的内部类代码和饿汉式中的序列化安全代码都是可以防御发射攻击的,当然会抛出相应异常,接下来我们介绍一下枚举单例模式

public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
} }

  枚举对象不能被反射创建,并且序列化与反序列化中枚举类型不会被创建出新的,下面看看枚举类型的构造器

protected Enum(String name,int ordinal){
this.name=name;
this.ordinal=ordinal;
}

  可见这个构造器是有参的,并且由这两个值确定了枚举唯一性,不会由序列化与反序列化破坏。并且也是线程安全的,原理同内部类。所以非常推荐枚举类型来完成单例模式。
5.源码解析:
  JDK中Runtime类就是一个单例模式,它不准外部创建实例,构造器代码如下:

/** Don't let anyone else instantiate this class */
private Runtime() {}

  并且还是饿汉式,代码如下:

private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}

  相信理解了上面的模式,可以很容易的明白这个类的设计模式

  当然还有我们常用的Spring框架,简单说一下就是Spring中对象创建在Bean作用域中仅创建一个,和我们上面讲的单例还是有稍许区别,这个单例的作用域是整个应用的上下文,通俗一点理解就是Spring就像一个商店,里面的商品一种只有一个,大家看见的一个商品都是同一个,这一种商品中不会再有另一个商品了。

结合JDK源码看设计模式——单例模式的更多相关文章

  1. 结合JDK源码看设计模式——桥接模式

    前言: 在我们还没学习框架之前,肯定都学过JDBC.百度百科对JDBC是这样介绍的[JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Jav ...

  2. 结合JDK源码看设计模式——原型模式

    定义: 指原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象.不需要知道任何创建的细节,不调用构造函数适用场景: 类初始化的时候消耗较多资源 new产生的对象需要非常繁琐的过程 构造函数比较 ...

  3. 结合JDK源码看设计模式——简单工厂、工厂方法、抽象工厂

    三种工厂模式的详解: 简单工厂模式: 适用场景:工厂类负责创建的对象较少,客户端只关心传入工厂类的参数,对于如何创建对象的逻辑不关心 缺点:如果要新加产品,就需要修改工厂类的判断逻辑,违背软件设计中的 ...

  4. 结合JDK源码看设计模式——模板方法模式

    前言: 相信很多人都听过一个问题:把大象关进冰箱门,需要几步? 第一,把冰箱门打开:第二,把大象放进去:第三,把冰箱门关上.我们可以看见,这个问题的答案回答的很有步骤.接下来我们介绍一种设计模式--模 ...

  5. 结合JDK源码看设计模式——适配器模式

    定义: 将一个类的接口转换成客户期望的另外一个接口(重点理解适配的这两个字),使得接口不兼容的类可以一起工作适用场景: 已经存在的类,它的方法和需求不匹配的时候 在软件维护阶段考虑的设计模式 详解 首 ...

  6. 结合JDK源码看设计模式——迭代器模式

    前言: Iterator翻译过来就是迭代器的意思.在前面的工厂模式中就介绍过了iterator,不过当时介绍的是方法,现在从Iterator接口的设计来看,似乎又是一种设计模式,下面我们就来讲讲迭代器 ...

  7. 结合JDK源码看设计模式——享元模式

    前言 在说享元模式之前,你一定见到过这样的面试题 public class Test { public static void main(String[] args) { Integer a=Inte ...

  8. 结合JDK源码看设计模式——建造者模式

    概念: 将一个复杂对象的构建与它的表示分离.使得同样构建过程可以创建不同表示适用场景: 一个对象有很多属性的情况下 想把复杂的对象创建和使用分离 优点: 封装性好,扩展性好 详解: 工厂模式注重把这个 ...

  9. 结合JDK源码看设计模式——观察者模式

    前言: 现在我们生活中已经离不开微信,QQ等交流软件,这对于我们来说不仅是交流,更有在朋友圈中或空间中进行分享自己的生活,同时也可以通过这个渠道知道别人的生活.我们在看朋友圈的时候其实我们扮演的就是一 ...

随机推荐

  1. 「速成应用」实在可靠的 微信小程序第三方代理加盟平台公司

    小程序,是基于微信平台的一个划时代产品,也就是嵌入到微信里的一个功能丰富.操作简洁的轻应用,不需要下载安装即可使用.不同的小程序,能实现不同的功能.例如,买电影票.餐厅排号.餐馆点菜.查询公交.查询股 ...

  2. Avio红外热像仪在汽车行业的应用

    红外热像仪利用红外探测器接受被测目标的红外辐射能量,将红外辐射能量转换成带有温度信息的图像信号,并通过显视屏等显示工具显示红外热图像.这种红外热图像与物体表面的温度分布相对应.红外热像仪能够将探测到的 ...

  3. ABP+AdminLTE+Bootstrap Table aspnetboilerplate 学习

    http://www.cnblogs.com/anyushengcms/p/7325126.html https://github.com/aspnetboilerplate/aspnetboiler ...

  4. [译文]Domain Driven Design Reference(三)—— 模型驱动设计的构建模块

    本书是Eric Evans对他自己写的<领域驱动设计-软件核心复杂性应对之道>的一本字典式的参考书,可用于快速查找<领域驱动设计>中的诸多概念及其简明解释. 其它本系列其它文章 ...

  5. java线程间通信之通过管道进行通信

    管道流PipeStream是一种特殊的流,用于在不同线程间直接传送数据,而不需要借助临时文件之类的东西. jdk中提供了四个类来使线程间可以通信: 1)PipedInputStream和PipedOu ...

  6. mysql分表经验总结

    一.为什么要分表? 当一张的数据达到几百万时,你查询一次所花的时间会变多,如果有联合查询的话,有可能会死在那儿了.分表的目的就在于此,减小数据库的负担,缩短查询时间.根据个人经验,mysql执行一个s ...

  7. redHat linux 修改防火墙设置简略版

    1) 重启后生效 开启: chkconfig iptables on 关闭: chkconfig iptables off 2) 即时生效,重启后失效 开启: service iptables sta ...

  8. iOS开发-程序的生命周期

    为了更好的管理程序,了解程序的生命周期是很有必要的. 运行结果: 1.首次启动: 2015-05-26 17:33:28.362 Live[4858:214241] 程序开始 2015-05-26 1 ...

  9. React从入门到放弃之前奏(2):React简介

    本系列将尽可能使用ES6(ES2015)语法.所以均在上节webpack的基础上做开发. React是Facebook开发的一款JS库,因为基于Virtual DOM,所以响应速度快,以及支持跨平台. ...

  10. C++使用类和对象

    1. 内置函数 程序调用函数时需要一定的时间和空间开销,其执行过程一般如下: 而C++提供了一种高效率的方法,即在编译的时候将所调用函数的代码直接嵌入到主函数中,而不是将流程转出去,这样可以避免函数调 ...