定义:

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

适用场景:

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

详解:

  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. 创建ndarray

    Numpy最重要的一个特点就是其N维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器,是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的. 创建数组最简单的方法就 ...

  2. PAT1007:Maximum Subsequence Sum

    1007. Maximum Subsequence Sum (25) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Y ...

  3. golang项目中使用条件编译

    golang项目中使用条件编译 C语言中的条件编译 golang中没有类似C语言中条件编译的写法,比如在C代码中可以使用如下语法做一些条件编译,结合宏定义来使用可以实现诸如按需编译release和de ...

  4. Linux时间子系统之三:jiffies

    1. jiffies背景介绍 jiffies记录了系统启动以来,经过了多少tick. 一个tick代表多长时间,在内核的CONFIG_HZ中定义.比如CONFIG_HZ=200,则一个jiffies对 ...

  5. 2018 Unite大会——《使用UPA工具优化项目》演讲实录

    2018年5月11日至13日,腾讯WeTest与Unity联合打造的移动游戏性能分析工具(Unity Performance Analysis,以下称为UPA)正式亮相2018 Unite大会,为Un ...

  6. Ubuntu物理机中解决VirtualBox虚拟机无法连接USB设备的问题

    本文由荒原之梦原创,原文链接:http://zhaokaifeng.com/?p=611 问题描述: 在安装完VirtualBox的USB控制器扩展(关于在VirtualBox中安装USB控制器扩展的 ...

  7. Java生成名片式的二维码源码分享

    世界上25%的人都有拖延症——但我觉得这统计肯定少了,至少我就是一名拖延症患者.一直想把“Java生成名片式(带有背景图片.用户网络头像.用户昵称)的二维码”这篇博客分享出来,但一直拖啊拖,拖到现在, ...

  8. matplotlib使用时报错RuntimeError: Python is not installed as a framework(一)

    笔者在第一次安装matplotlib后运行时出现报错. import matplotlib as mlb from matplotlib import pylab as pl x = [1,3,5,7 ...

  9. ubuntu旧版本源失效的处理方法

    (1)先备份 cp /etc/apt/sources.list /etc/apt/sources.list_backup (2)更换源 在ubuntu的网站中,提供了一个源供那些不再提供支持的版本使用 ...

  10. spring(一)--spring/springmvc/spring+hibernate(mybatis)配置文件

    这篇文章用来总结一下spring,springmvc,spring+mybatis,spring+hibernate的配置文件 1.web.xml 要使用spring,必须在web.xml中定义分发器 ...