Java设计模式---单例模式解析与实战

160 阅读8分钟

#定义 确保某一个类只有一个实例, 而且自行实例化并向整个系统提供这个实例

#使用场景 确保某个类有且只有一个对象的场景, 避免产生多个对象消耗过多的资源, 或者 某种类型的对象只应该有且只有一个。 例如, 创建一个对象需要消耗的资源过多, 如要访问IO和数据库等资源,这时就要考虑使用单例模式。

#单例模式UML类图

  • 角色:

(1)Client——高层客户端; (2)Singleton——单例类。

  • 实现单例模式的关键点: (1)构造函数不对外开放,一般为Private; (2)通过一个静态方法或者枚举返回单例类对象; (3)确保单例类的对象有且只有一个,尤其是在多线程环境下; (4)确保单例类对象反序列化时不会重新构建对象

  • 通过将单例类的构造函数私有化, 使得客户端代码不能通过 new 的形式手动构造单例类的对象

  • 单例类会暴露一个公有静态方法客户端需要调用这个静态方法获取到单例类唯一对象

  • 在获取这个单例对象的过程中需要确保线程安全, 即在多线程环境下构造单例类的对象也是有且只有一个, 这也是实现的难点


#重点,注意单例模式中 volatile的重要性 - **[Android(Java) | 如何使程序实现线程安全(拓展关键词:ThreadLocal、重排序、volatile/final)](https://www.jianshu.com/p/5de156a80c02)** ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d5228f7eee6b46f3b2d3f73e89d0bfd4~tplv-k3u1fbpfcp-zoom-1.image)
#单例的几种实现方式 #1. 饿汉模式 **声明一个`静态类对象`,在`声明时`就己经`初始化`, 用户调用`类对象get方法`时,可以直接拿去用; 【一声明就初始化,所谓“饿”】** 如下, CEO类使用了饿汉单例模式; ``` /** * 普通员工 */ class Staff { public void work() { // 干活 } } // 副总裁 class VP extends Staff { @Override public void work() { // 管理下面的经理 } } // CEO, 饿汉单例模式 class CEO extends Staff { private static final CEO mCeo = new CEO();
// 构造函数私有
private CEO() {
}

// 公有的静态函数,对外暴露获取单例对象的接口
public static CEO getCeo() {
    return mCeo;
}
@Override
public void work() {
    // 管理VP
}

} // 公司类 class Company { private List allPersons = new ArrayList();

public void addStaff(Staff per) {
    allPersons.add(per);
}

public void showAllStaffs() {
    for (Staff per : allPersons) {
        System.out.println("Obj : " + per.toString());
    }
}

} public class Test { public static void main(String[] args) { Company cp = new Company(); // CEO对象只能通过getCeo函数获取 Staff ceo1 = CEO.getCeo(); Staff ceo2 = CEO.getCeo(); cp.addStaff(ceo1); cp.addStaff(ceo2); // 通过new创建VP对象 Staff vp1 = new VP(); Staff vp2 = new VP(); // 通过new创建Staff对象 Staff staff1 = new Staff(); Staff staff2 = new Staff(); Staff staff3 = new Staff();

    cp.addStaff(vp1);
    cp.addStaff(vp2);
    cp.addStaff(staff1);
    cp.addStaff(staff2);
    cp.addStaff(staff3);

    cp.showAllStaffs();
}

}

#2. 懒汉模式
- **懒汉模式是声明一个静态对象,
并且在用户`第一次`调用`getInstance``才`进行`初始化`;
【“拖延”,等到调用才初始化,所谓“懒”!】**
public class Singleton {
    private volatile static Singleton instance;
    private Singleton () {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton ();
        }
        return instance;
    }
}
- **`getInstance()`中添加了 `synchronized` 关键字,
也就是 `getInstance()`是一个同步方法,
即上面所说的在`多线程`情况下保证`单例对象唯一性`的手段。**

- **只不过这里可能有一个问题,
即使`instance`己经被初始化(第一次调用时就会被初始化instance),
`每次`调用`getInstance`方法都会进行`同步`,
这样会消耗不必要的资源,这也是懒汉单例模式存在的最大问题。**

- **优点:单例只有在使用时才会被实例化,在一定程度上`节约了资源`;**
- **缺点:第一次加载时需要及时进行实例化,反应稍慢,
最大的问题是每次调用 getInstance都进行`同步`,造成不必要的`同步开销`。**

**这种模式一般不建议使用!!!!!!!!!!**


#3. DoubleCheckLock(DCL)实现单例【双重校验锁】
- **优点:
资源利用率高,
第一次执行`getInstance()`时单例对象才会被实例化,效率高。
既能够在`需要时才初始化`单例,
又能够保证`线程安全`,
且单例对象初始化后每次调用`getInstance()`不进行`同步锁`,
减少不必要的`同步开销`:**
- **缺点:第一次加载时反应稍慢,
也由于 Java 内存模型的原因偶尔会失败。
在高并发环境下也有一定的缺陷,虽然发生概率很小。**

public class Singleton { private volatile static Singleton sInstance = null; private Singleton() { } public void doSomething() { System.out.println("do sth."); } public static Singleton getInstance() { if (mInstance == null) { synchronized (Singleton.class) { if (mInstance == null) { sInstance = new Singleton(); } } } return sInstance; }

- **static 保证单例;
volatile 禁止重排序;
getInstance() 用来获取实例;
synchronized 保证原子性、可见性、线程安全;**
- **亮点:`getInstance()`方法中对`instance`进行了两次判空:
第一层判断主要是为了避免不必要的同步【有实例则直接返回,没必要同步】,
第二层的判断则是为了在`null的情况``创建实例`
【可能第一层与第二层判断中途有其他线程初始化完成了单例,
单例不为`null`,就不用创建了】:<br>
假设线程A和线程B先后访问了`getInstance() `;
线程A执行到`sInstance = new Singleton()`语句,
这里看起来是一句代码,但实际上它并不是一个`原子操作`,
这句代码最终会被编译成多条汇编指令,它大致做了`3件`事情:
(1)给Singleton的实例`分配内存`;
(2)调用`Singleton()``构造函数``初始化`成员字段;
(3)将`sInstance对象`指向分配的内存空间(此时sInstance就不是`null`了)。<br>
但是,由于Java编译器允许处理器`乱序执行`,
以及JDK1.5之前`JMM(Java Memory Model,即Java内存模型)`Cache、
寄存器到主内存回写顺序的规定,
上面的第二和第三的顺序是`无法保证`的。【指令重排序】<br>
即,执行顺序可能是1-2-3也可能是1-3-2。
如果是后者,并且在`3执行完毕``2未执行之前`,被切换到`线程B`上,
这时候`sInstance`因为己经在线程A内执行过了第三点,
`sInstance`己经是`非空`了,
所以,
`线程B`通过`getInstance() `直接取走`sInstance`,
再使用时就会`出错`,这就是`DCL失效问题`,
而且这种难以跟踪难以重现的错误很可能会隐藏很久。
在JDK1.5之后,SUN官方己经注意到这种问题,
调整了JVM,具体化了`volatile`关键字,
因此,
如果JDK1.5或之后的版本,
只需要将`sInstance`的定义改成`private volatile static Singleton sInstance = null`就可以保证sInstance对象`每次都是从主内存中读取`,
就可以使用DCL的写法来完成`单例模式`。
当然,`volatile` 或多或少也会影响到`性能`,
但考虑到程序的`正确性`,牺牲这点性能还是值得的。^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^**

- **DCL 模式是`使用最多`的单例实现方式!!!!
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
它能够在`需要时才`实例化单例对象,
并且能够在绝大多数场景下保证单例对象的`唯一性`,
除非代码在并发场景比较复杂或者低于JDK 6版本下使用,
否则,这种方式一般能够满足需求。**

<br>
#4. 静态内部类单例模式
- **DCL虽然在一定程度上解决了`资源消耗、多余的同步、线程安全`等问题,
但是,它还是在某些情况下出现失效的问题。
就是刚说的`双重检查锁定(DCL)失效`;**
- **在《Java 并发编程实践》一书的最后谈到了这个问题,
并指出这种“优化”是`丑陋`的,`不赞成使用`。
而建议使用如下的代码`替代`:**

public class Singleton { private Singleton() { } public static Singleton getInstance () { return SingletonHolder.sInstance; } /** * 静态内部类 */ private static class SingletonHolder { private static final Singleton sInstance = new Singleton(); } }

- **当第一次加载`Singleton类`时并不会初始化`sInstance`,
只有在第一次调用`Singleton``getInstance()``sInstance`才会被初始化!!!
因此,
第一次调用`getInstance()`会导致虚拟机加载`SingletonHolder类`,
这种方式不仅能够确保`线程安全`,
也能够保证单例对象的`唯一性`,同时也`延迟`了单例的`实例化`,
所以这是推荐使用的单例模式实现方式。**

<br>
#5. 枚举单例
除了以上几种方式,还有更简单的实现方式——枚举!:

public enum SingletonEnum { INSTANCE; public void doSomething() { System.out.println("do sth."); } }

- **优点突出:写法简单;<br>
`枚举`在Java中与普通的类是一样的,
不仅能够有字段,还能够有自己的方法。<br>
最重要的是`默认枚举实例``创建``线程安全`的,
并且在`任何情况下`它都是一个`单例`。<br>
在上述的几种单例模式实现中,
在一个情况下它们会出现`重新创建对象`的情况,那就是`反序列化`。<br>
通过`序列化`可以将`一个单例``实例对象`写到`磁盘`,
然后再`读回来`,从而有效地`获得一个实例`。<br>
即使`构造函数``私有`的,
`反序列化`时依然可以通过特殊的途径去创建类的一个`新的实例`,
相当于调用该类的`构造函数``反序列化`操作提供了一个很特别的`钩子函数`,
类中具有一个`私有的、被实例化`的方法`readResolve()`,
这个方法可以让开发人员控制对象的`反序列化`。
例如,
上述几个示例中如果要杜绝单例对象在被反序列化时重新生成对象,
那么必须加入如下方法:**

private Object readResolve() throws ObjectStreamException { return sInstance; }

**即在`readResolve()`中将`sInstance对象`返回,
而不是默认的重新生成一个新的对象。
而对于枚举,并不存在这个问题,
因为即使`反序列化`它也不会`重新生成`新的实例。**



<br><br><br><br><br><br>


---
参考:
- 《Android源码设计模式解析与实战》