场景:有的情况下,我们只需要创建一个实例,以此来节省创建和销毁的消耗,以及保持该实例的稳定性(不希望被外部破坏或篡改)
此时我们可以应用单例模式来创建该实例的对象,下面列举几种方法,并且说明其各自的使用场景和优缺点:
饿汉模式
顾名思义,我很饥饿,我上来就创建该实例,不惯用不用得着,并且以后都使用该实例。示例代码:
class Mgr {
private static final INSTANCE = new Mgr();
private Mgr() {}
public static Mgr getInstance() {
return INSTANCE;
}
}
关键代码说明:
- 构造器是私有的,这样就避免了被外部实例化
- 属性加上final属性之后,也可以避免被再次赋值,而像通过下面的静态代码块来初始化,就不能加上final修饰符来限制
// 省略
private static INSTANCE;
static {
INSTANCE = new Mgr();
}
// 省略
懒汉模式 - 双重锁
饿汉模式比较简单,但是有的时候实例化一次资源如果消耗很大的话,我们就希望在对象被实际使用到(调用)的时候才进行实例化,我们很容易想到下面的代码实现:
class Mgr {
private static INSTANCE;
private Mgr() {}
public static Mgr getInstance() {
if(INSTANCE == null) {
INSTANCE = new Mgr();
}
return INSTANCE;
}
}
看起来似乎不错,但在并发多线程场景下,进行对象实例化的代码INSTANCE = new Mgr();
是有可能被同时执行到的,这样一来,就不满足单例的要求了。
怎么办呢?熟悉多线程的工程师会给这个实例化方法加上一个锁来解决这个问题,像下面这样:
// 省略
public static syncronized Mgr getInstance() {
if(INSTANCE == null) {
INSTANCE = new Mgr();
}
return INSTANCE;
}
// 省略
开启多个线程测试也能通过,但仍然有缺陷,因为加锁就以为着,这个方法被所有线程一个一个的排队去使用,多线程失去了意义,性能损失很大。
接着往下想,怎么办?能不能在需要进行实例化的代码块上进行精准加锁呢?像下面这样
// 省略
public static syncronized Mgr getInstance() {
if(INSTANCE == null) {
syncronized(Mgr.class) {
INSTANCE = new Mgr();
}
}
return INSTANCE;
}
// 省略
这样是不是就完美了?实际还有问题的,如果多个线程同时执行了代码if(INSTANCE == null)
并且都等待锁去做对象实例化,就还会new出来多个实例。于是,下面就引出了经典的双重锁方式
// 省略
public static syncronized Mgr getInstance() {
if(INSTANCE == null) {
syncronized(Mgr.class) {
if(INSTANCE == null) {
INSTANCE = new Mgr();
}
}
}
return INSTANCE;
}
// 省略
这样一来,即便是有多个线程拿到了锁要去做对象的实例化,还会再次检查一下对象是否为空才会执行实例化逻辑。因为第一个拿到锁线程一定已经完成了对象的实例化,其他拿到锁的线程检查if(INSTANCE == null)
的时候就一定是false了。
懒汉模式 - 静态内部类
在Effective Java这本书中,作者提到了更为简洁的时间懒汉模式的方法——通过静态内部类。我们将上述代码改造一下
class Mgr {
private Mgr() {}
private static class MgrHelpler {
private static final INSTANCE = new Mgr();
public static Mgr getInstance() {
return INSTANCE;
}
}
public Mgr getInstance() {
return MgrHelpler.getInstance();
}
}
看起来简洁多了。但是稍微难理解一点,这利用了JVM类加载的机制,在加载类的时候,静态内部类MgrHelpler
会被加载,但内部的静态属性 INSTANCE
只有在被使用到的时候,才会被实例化,后续再调用就直接使用该实例了。
懒汉模式 - 枚举属性
还有一种预发层面天然支持,就是枚举,枚举的构造器默认就是private的,因此可以很好的避免外部的篡改,另外枚举的属性只有在使用到时候,才会进行初始化,因而也可以避免使用前的资源消耗。上面的代码可以修改为以下简单的几行
enum Mgr {
INSTANCE
Mgr() {}
}
so easy,当然实际场景实例化过程会添加到构造器当中。
总结
上面介绍了4种可行的构建单例的模式:
- 饿汉模式
- 懒汉模式 - 双重锁
- 懒汉模式 - 静态内部类
- 懒汉模式 - 枚举属性
一般来讲,没有特殊的考虑,使用饿汉模式即可,简单易于理解,如果处于性能考虑,3中懒汉模式可以根据实际情况进行使用。
笔者原文发布在CSDN,欢迎点击查看:https://blog.csdn.net/fuhua_chou/article/details/124553365
也可以关注笔者:请给我一根烟的时间(https://blog.csdn.net/mytream),查看更多个人心得和分享