一文搞懂设计模式中的单例模式
作者:wh柒八九
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的意义
通常面试官会很笼统的问你,什么是单例模式?单例模式用来解决了什么痛点?没有单例模式我们会怎么办?单例模式他有什么缺点吗?
单例模式是最简单的设计模式之一,属于创建型模式,它提供了一种创建对象的方式,确保只有单个对象被创建。
这个设计模式主要目的是想在整个系统中只能出现类的一个实例,即一个类只有一个对象。
单例模式的解决的痛点就是节约资源,节省时间从两个方面看:
- 由于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是很重要的.
- 因为不需要频繁创建对象,我们的GC压力也减轻了,而在GC中会有STW(stop the world),从这一方面也节约了GC的时间
单例模式的缺点:
简单的单例模式设计开发都比较简单,但是复杂的单例模式需要考虑线程安全等并发问题,引入了部分复杂度。
扩展:从你的回答中能进行哪些扩展呢?我们谈到了GC,有可能这时候就会问你GC,STW等知识。谈缺点的时候谈到了复杂的单例模式,
这个时候可能会问你让你设计一个优秀的单例模式你会怎么设计,会怎么实现?
单例模式的设计
通常这里面试官会问你单例模式怎么设计,需要看重哪些方面?一般来说单例模式有哪些实现方式?
通常单例模式在Java语言中,有两种构建方式:
- 饿汉方式。指全局的单例实例在类装载时构建
- 懒汉方式。指全局的单例实例在第一次被使用时构建。
不管是那种创建方式,它们通常都存在下面几点相似处:
单例类必须要有一个 private 访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化; instance 成员变量和 uniqueInstance 方法必须是 static 的。
设计单例模式的时候一般需要考虑几种因素:-线程安全 -延迟加载 -代码安全:如防止序列化攻击,防止反射攻击(防止反射进行私有方法调用) -性能因素
一般来说,我们去网上百度去搜大概有7,8种实现,下面列举一下需要重点知道的饿汉,懒汉(线程安全,线程非安全),双重检查(DCL)(重点),内部类,以及枚举(重点),
下面比对下各个实现:
名称 | 线程安全 | 并发性能好 | 可以延迟加载 | 序列化/反序列化安全 | 能抵御反射攻击 |
饿汉式 | Y | Y | N | N | N |
懒汉式(不加锁) | N | Y | Y | N | N |
懒汉式(加锁) | Y | N | Y | N | N |
DCL | Y | Y | Y | N | N |
静态内部类 | Y | Y | Y | N | N |
枚举 | Y | Y | N | Y | Y |
扩展:
我们上面说到了各个模式的实现,这个时候很有可能会叫你手写各个模式的代码。当然也有可能会问你线程安全,代码安全等知识。
饿汉模式
public class Singleton { private static Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; } }
所谓 “饿汉方式” 就是说JVM在加载这个类时就马上创建此唯一的单例实例,不管你用不用,先创建了再说
如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。
饿汉模式代码比较简单,对象在类中被定义为private static,通过getInstance(),通过java的classLoader机制保证了单例对象唯一。
扩展:
有可能会问instance什么时候被初始化?
Singleton类被加载的时候就会被初始化,java虚拟机规范虽然没有强制性约束在什么时候开始类加载过程,但是对于类的初始化,虚拟机规范则严格规定了有且只有四种情况必须立即对类进行初始化,遇到new、getStatic、putStatic或invokeStatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
生成这4条指令最常见的java代码场景是:
1)使用new关键字实例化对象
2)读取一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
3)设置一个类的静态字段(被final修饰、已在编译期把结果放在常量池的静态字段除外)
4)调用一个类的静态方法
class的生命周期?
class的生命周期一般来说会经历加载、连接、初始化、使用、和卸载五个阶段
class的加载机制
这里可以聊下classloader的双亲委派模型。
懒汉式(非线程安全和synchronized关键字线程安全版本 )
public class Singleton { private static Singleton uniqueInstance; private Singleton (){ } //没有加入synchronized关键字的版本是线程不安全的 public static Singleton getInstance() { //判断当前单例是否已经存在,若存在则返回,不存在则再建立单例 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } }
所谓 “饿汉方式” 就是说单例实例在第一次被使用时构建,而不是在JVM在加载这个类时就马上创建此唯一的单例实例。
但是上面这种方式很明显是线程不安全的,如果多个线程同时访问getInstance()方法时就会出现问题。
如果想要保证线程安全,一种比较常见的方式就是在getInstance() 方法前加上synchronized关键字,如下:
public static synchronized Singleton getInstance() { if (instance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; }
我们知道synchronized关键字偏重量级锁。
虽然在JavaSE1.6之后synchronized关键字进行了主要包括:
为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升。
但是在程序中每次使用getInstance() 都要经过synchronized加锁这一层,这难免会增加getInstance()的方法的时间消费,而且还可能会发生阻塞。我们下面介绍到的 双重检查加锁版本 就是为了解决这个问题而存在的。
懒汉式(双重检查加锁版本)
利用双重检查加锁(double-checked locking),首先检查是否实例已经创建,如果尚未创建,“才”进行同步。这样以来,只有一次同步,这正是我们想要的效果。
public class Singleton { //volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理uniqueInstance变量 private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getInstance() { //检查实例,如果不存在,就进入同步代码块 if (uniqueInstance == null) { //只有第一次才彻底执行这里的代码 synchronized(Singleton.class) { //进入同步代码块后,再检查一次,如果仍是null,才创建实例 if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
synchronized同步块里面能够保证只创建一个对象。但是通过在synchronized的外面增加一层判断,就可以在对象一经创建以后,不再进入synchronized同步块。这种方案不仅减小了锁的粒度,保证了线程安全,性能方面也得到了大幅提升。
同时这里要注意一定要说volatile,这个很关键,volatile一般用于多线程的可见性,但是这里是用来防止指令重排序的。
很明显,这种方式相比于使用synchronized关键字的方法,可以大大减少getInstance() 的时间消费。
懒汉式(登记式/静态内部类方式)
静态内部实现的单例是懒加载的且线程安全。
只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance(只有第一次使用这个单例的实例的时候才加载,同时不会有线程安全问题)。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
饿汉式(枚举方式)
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。 它更简洁,自动支持序列化机制,绝对防止多次实例化 (如果单例类实现了Serializable接口,默认情况下每次反序列化总会创建一个新的实例对象,同时这种方式也是《Effective Java 》以及《Java与模式》的作者推荐的方式。
public enum Singleton { //定义一个枚举的元素,它就是 Singleton 的一个实例 INSTANCE; public void doSomeThing() { System.out.println("枚举方法实现单例"); } }
使用方法:
public class ESTest { public static void main(String[] args) { Singleton singleton = Singleton.INSTANCE; singleton.doSomeThing();//output:枚举方法实现单例 } }
扩展阅读:
《Effective Java 中文版 第二版》
这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。 —-《Effective Java 中文版 第二版》
《Java与模式》
《Java与模式》中,作者这样写道,使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
本文小结
单例模式虽然看起来简单,但是设计的Java基础知识非常多,如static修饰符、synchronized修饰符、volatile修饰符、enum等。
这里的每一个知识点都可以变成面试官下手的考点,而单例只是作为一个引子,考到最后看你到底掌握了多少。看你的广度和深度到底是怎么样的。
到此这篇关于一文搞懂设计模式中的单例模式的文章就介绍到这了,更多相关设计模式中的单例模式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!