码恋

ALL YOUR SMILES, ALL MY LIFE.

莞尔君笑,倾我余生
  menu

设计模式系列之单例模式

最近看了一些框架的底层源码,了解了其中令人惊叹的设计模式,愈发感觉一个好的设计会给我们的代码带来多么灵活的扩展性。

所以决定近期把各类设计模式好好整理一下,形成知识体系。而这篇要说的是比较入门的单例模式。

那首先来反问三连,什么是单例模式?单例模式解决了什么问题?单例模式应用场景?怎么实现单例模式?

不好意思,多了一连😆 ,下面,我们一一讨论。

什么是单例模式

单例模式属于创建型模式,它提供了一种创建对象的方式,确保只有单个对象被创建。这个设计模式主要目的是想在整个系统中只能出现类的一个实例,即一个类只有一个对象

比较官方的一段话,可以说解释的很清楚了。

解决了什么问题

由于在单例模式中,一个类只有一个对象,从其特性我们可以很自然的想到单例模式可以
节约空间,节约时间。

  • 对于频繁使用的对象,尤其是比较复杂的对象创建,节省了创建对象的时间。
  • 因为不需要频繁创建对象,我们的GC压力也减轻了,而在GC中会有STW(stop the world),从这一方面也节约了GC的时间。

结合单列模式的特点,可以顺理成章的知道它的应用场景。

实现方式

那么我们怎么实现一个单例模式呢,我们来思考我们的目标---保证在一个应用中一个类只有一个对象。

于是,我们就有下边的步骤:

  • 构造方法私有化
  • 在本类中创建一个对象
  • 定义一个public方法,在其他程序使用时提供这个已经创建好的对象

饿汉式

【可以使用】

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}
  • 优点
    实现简单,由于在类加载时就完成了实例化对象,没有线程安全问题。
  • 缺点
    由于在类加载时就实例化对象,如果后面我们没有用到这个对象,就造成了对资源的浪费。当然,可以忽略不计。

饿汉式的;另一种写法:
【可以使用】

public class Singleton {
    private static Singleton instance = null;

    static {
        instance = new Singleton();
    }

    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

懒汉式

也称饱汉式,与饿汉式不同的是,类的实例在获得的方法中创建。
【线程不安全,不可使用】

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

显然,这样写并不保证线程安全。比如有这样两个线程A,B,它们都去调用 getInstance( )方法去拿这个类的对象。由于一开始没有做过实例化,instance为null,A线程在执行完if (null == instance)后,这时线程B也执行到此处,那A线程还没有完成类的实例化,instance还是null,故两个线程都调用了new去实例化了两个对象。😭

那到这里,就会很简单的想到加锁来解决线程安全的问题了。
【效率低下,不推荐使用】

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

观察上面的代码,我们可以看到,在每次调用getInstance() 时,即使该类已经实例化,还是要去做同步,而这种情况只要return就行了。

那思考到这里,顺理成章的能想到,既然这样锁在方法上面效率低下,那就缩小锁的范围,所在这个类上面。于是有下面的实现:

【线程不安全,不可以使用】

public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (null == instance) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

嗯,完美!但是仔细看一看,好像哪里不对啊。👿在多线程场景下,我们再捋一下这块代码的执行流程。同样有这样两个线程A,B,它们都去调用 getInstance( )方法去拿这个类的对象。由于一开始没有做过实例化,instance为null,A线程在执行完if (null == instance)后,拿到了锁。这时线程B也执行到此处,但是由于被锁了,就等待A线程锁的释放。A线程执行new实例化了Singleton,执行完毕后,释放了锁。那B又得到了锁,继续执行Singleton的实例化,悲剧!!!两个线程都调用了new去实例化了两个对象。😭

经过一代又一代人的不懈努力,在此基础上,又有了下面的完美解决方式:

懒汉式双重校验锁

【可以使用】

public class Singleton {
    private volatile static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (null == instance) {
            synchronized (Singleton.class) {
                if (null == instance) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

继续上面捋代码的逻辑,等到A线程实例化完成后,释放了锁。这时B线程执行到if ( null == instance ),由于A线程已经完成了实例化,故直接返回Instance。

这里需要注意的是volatile关键字,JVM会有指令重排的情况,会造成获取到的对象没有被实例化。使用该关键字,可以保证修饰的关键字前后执行顺序唯一,不会进行指令重排。

内部类

【可以使用】

public class Singleton {

    private Singleton() {
    }
    
    private static class SingletonHolder{
        private static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

与饿汉式相似,内部类的实现方式都通过在类加载时实例化的方式保证线程安全。

不同点是饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用。但是静态内部类的实现方式在Singleton类被装载时并不会被立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonHolder类,从而完成Singleton的实例化。类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。与饿汉式相比,这种实现方式既没有浪费资源,又能保证线程安全。

枚举

【推荐使用】

public enum SingletonEnum {

    instance;

    private SingletonEnum() {
    }

    public void method() {

    }
}

class Aa {
    public static void main(String[] args) {
        SingletonEnum.instance.method();
    }
}

在最后比较推荐使用枚举方式实现单例模式,IDEA上新建单例类使用的是第一种饿汉式方法。


另,知乎上有一篇比较有意思的对单例模式的讲解可以参考:链接

❤ 转载请注明本文地址或来源,谢谢合作 ❤

wx.png


标题:设计模式系列之单例模式
作者:wangning1018
地址:https://aysaml.com/articles/2019/10/14/1571040307396.html