引子
最近一段时间在准备面试,为了面试于是准备重新复习一下设计模式,同时我相信这也是一个很好的重新学习的机会。
为什么需要单例模式
单例模式算得上是我第一个接触到的设计模式,那还是在大学期间在学习spring框架的时候老师为了说明spring的优点于是举了一个单例对象和多例对象的例子,但由于那个时候水平很菜(虽然现在也菜)并不能完全理解,于是在课后查询了资料,对单例模式也算有了一个初步认识,貌似扯远了那下面我们直接开始。
我们都知道Java可以通过new进行对象创建,当我们需要使用一个对象的方法时就会进行new,比如
Object obj = new Object();,要知道Java每次进行一次new操作时都会在堆内存创建一个对象,可能对于没接触过JVM的同学来说有点难以理解,可以结合下面这张图:
这张图画的比较简单主要是便于理解想要详细了解的可以去看看JVM方面的知识,可以看到每个对象都占用了一定的内存空间,但是现实是很多情况下,对于某些对象我们只需要调用它的一些方法,其中并没有状态的变化,每次使用都创建一个新的对象对于内存空间实在有些浪费,要知道内存是很宝贵的资源。为了解决这个问题便诞生了单例模式,我们还是用一张图说明,单例模式下对对象的使用如下:
可以看到我们只创建了一个对象,这个对象被多方调用,很好的节省了内存空间。
单例模式的实现
了解了由来就要开始动手实现了,只有动手写了才能真正理解。
饿汉式
代码实现如下:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
这段代码的要点如下:
- 创建了一个私有的静态对象,在类加载之后就会创建对象
- 构造方法私有化,保证不能被外部对象构造
- 提供静态方法用于获取对象
这样一个简单的单例模式就实现了,但是这段代码也还有能够优化的点,首先如它的名字饿汉式那样在类加载之后就会初始化对象,这样虽然能够保证它的线程安全,但是如果我们并没有使用这个对象,它却依然在内存中占用了空间,那么有没有办法能够让我们在需要的时候才初始化呢?出于这个需要于是懒汉式就诞生了。
懒汉式
我们还是先看代码:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance==null){
return new Singleton();
}
return instance;
}
}
对比饿汉式,可以看到只有当我们调用静态方法获取对象时才会初始化,这样就解决了饿汉式的内存占用问题,但是这段代码就完美了吗?我们可以套用其它的场景来看这段代码,比如在多线程情况下会不会有线程安全问题呢?
假设有线程A,B,线程A首先调用方法,当走到instance==null后(注意此时因此通过了判断)因为调度策略线程被切换了,此时线程B进入,一路执行下去并且创建了对象,最后结束。调度又重新回到了线程A,A继续往下执行,又创建了一个对象。就出现了线程安全问题。
接触过并发编程的人都知道,如果出现并发问题了我们就加锁(当然这不是说并发问题只能通过加锁解决)。
懒汉式(线程安全)
实现如下:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance==null){
return new Singleton();
}
return instance;
}
}
我们在静态方法上加了一把锁,在调用方法之前需要获得锁保证了只能有一个线程进入初始化,解决了并发问题。
同样的我们还可以问一问自己,这种实现方式会有什么问题呢?
锁在方法上意味着每次调用方法都需要获取锁,即使对象已经实例化依然需要获取锁,要知道对于锁的使用,我们锁住的范围应该尽可能的小,那么对于这种方法我们是不是能够只锁方法内的部分代码呢?
懒汉式双重校验
部分加锁的代码实现如下:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance==null) {
synchronized (Singleton.class) {
if (instance ==null){
return new Singleton();
}
}
}
return instance;
}
}
可以看到我们首先判断对象是否被实例化,如果没有被实例化才会进入锁的部分,这样在对象实例化之后就不用重复获取锁提高了并发度,那么为什么在内部还要判空呢?
我们可以通过反证法来理解这个双重校验的模式。在没有内部加判断的前提下,假设有两个线程A,B,首先线程A通过外部判断,未获取锁,然后时间片切换,线程B执行,线程B一路执行完成,此时切换回线程A获取了锁,由于没有判空直接创建了对象。由此可见,外部的判断是为了避免对象实例化后重复获取锁的消耗,内部的判断是为了避免对象的重复创建。
看起来好像已经没什么问题了,但是上面的代码真的线程安全吗?要知道Java是有乱序执行机制的(不清楚的可以搜索了解一下),如果在这种机制下上面的代码会没有问题吗?
我们可以模拟一下过程,要知道instance=new Singleton()其实可以拆分为三步:
- instance分配空间
- 对象初始化
- instance指向内存中的对象 正常来说应该是1->2->3的执行顺序,但是由于乱序执行的机制,很可能会变成1->3->2,如果是单线程自然不会有问题,如果是多线程,线程A只执行了1->3,此时线程B调用方法发现实例不为空则直接获取内存中的空间,然而此时对象还没有初始化。
既然问题是指令乱序执行引起的那么禁止就好了,Java中提供了volatile关键字,使用后就能禁止指令的重排序,在添加后我们的最终版本代码如下:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance==null) {
synchronized (Singleton.class) {
if (instance ==null){
return new Singleton();
}
}
}
return instance;
}
}
乱序执行是为了提高性能才使用的,使用vlotile自然会有性能损耗,加锁也会带来损耗,如果在突然有大量并发的情况下很可能会导致大量线程等待锁,虽然不会有并发问题,但是占用资源,那么有没有办法不使用锁也能保证线程安全呢?
静态内部类实现(线程安全)
老规矩先看代码:
public class Singleton {
private Singleton() {
}
public static class SingletonHandler {
private static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHandler.instance;
}
}
通过创建静态内部类的方式,这样就避免了加载时将实例加载到内存中,同时通过JVM保证实例只会被初始化一次,即做到了延迟实例化,又保证了线程安全。
枚举类实现
代码实现如下:
public enum Singleton {
INSTANCE;
public void doSomeThing(){
}
}
基于枚举的实现,线程安全,支持单例,防反射与反序列化。
总结与参考
在文章中我分享了对单例模式的理解与一些常用的实现方式,学习设计模式能够帮助我们写出质量更高的代码,虽然比较基础但是学习是个循序渐进的过程,一步一个脚印才能走的更远更稳。
以下是文章参考的内容:
-
《大话设计模式》
-
代码随想录知识星球精华-大厂面试八股文第二版