结合JDK源码看设计模式——单例模式

定义:

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

适用场景:

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

详解:

  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就像一个商店,里面的商品一种只有一个,大家看见的一个商品都是同一个,这一种商品中不会再有另一个商品了。

(0)

相关推荐

  • 设计模式笔记(一):Singleton 设计模式

    今天开始学习设计模式,借此机会学习并整理学习笔记. 设计模式是一门不区分语言的课程,什么样的编程语言都可以用到设计模式.如果说java语法规则比作武功招式的话,那么设计模式就是心法. 设计模式共有23 ...

  • 详解JAVA面向对象的设计模式 (一)、单例模式

    本系列,记录了我深刻学习设计模式的过程.也算是JAVA进阶学习的一个重要知识点吧. 与设计相关的代码会贴出,但是基础功能的代码会快速带过.有任何错误的地方,都欢迎读者评论指正,感谢.冲冲冲! 单例模式 ...

  • 面试高频-吃透单例设计模式

    单例设计模式 单例设计模式的介绍 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例, 并且该类只提供一个取得其对象实例的方法(静态方法). 比如 Hiber ...

  • 单例模式

    什么是Singleton? 单例设计模式,即某个类在整个系统中只能有一个实例对象可被获取和使用的代码模式. 例如:代表JVM运行环境的Runtime类. 要点 一是某个类只能有一个实例 构造器私有化 ...

  • java常见设计模式之---单例模式

    java常见设计模式之---单例模式 1.单例模式简介 应用场景举例 2.单例模式的特点 3.单例模式和静态类 4.单例模式的经典实现 饿汉式单例(典型实现) 饿汉式-静态代码块 懒汉式单例创建,五种 ...

  • 23种设计模式入门 -- 单例模式

    单例模式:采用一定的方法,使得软件运行中,对于某个类只能存在一个实例对象,并且该类只能提供一个取得实例的方法. 分类: 饿汉式 静态常量方式 静态代码块方式 懒汉式 普通方式,线程不安全 同步方法方式 ...

  • 单例-怎么用单例

    单例的实现 在实现一个单例时,需要从如下角度来考虑单例的实现是否合理: 1.不应该提供创建多个实例的接口.主要是两部分内容,分别是要将构造函数设为private属性,防止在别处通过new创建实例:在创 ...

  • 这几种单例模式写法你知道几种

    这几种单例模式写法你知道几种

  • 设计模式:单例模式 (关于饿汉式和懒汉式)

    定义 单例模式是比较常见的一种设计模式,目的是保证一个类只能有一个实例,而且自行实例化并向整个系统提供这个实例,避免频繁创建对象,节约内存. 单例模式的应用场景很多, 比如我们电脑的操作系统的回收站就 ...

  • Java设计模式之单例模式

    单例模式,是特别常见的一种设计模式,因此我们有必要对它的概念和几种常见的写法非常了解,而且这也是面试中常问的知识点. 所谓单例模式,就是所有的请求都用一个对象来处理,如我们常用的Spring默认就是单 ...