本文已参与「新人创作礼」活动,一起开启掘进创作之路。
单例模式
单例模式是 Java 中最简单,也是最基础,最常用的设计模式之一。在运行期间,保证某个类只创建一个实例,保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式分为饿汉模式和懒汉模式。
定义
确保一个类只有一个实例,并提供一个全局访问点
饿汉模式
所谓饿汉模式,是指在类加载的时候立即初始化,并创建单例对象。它能保证绝对线程安全,在线程还没有出现之前就实例化了。
代码实现
package com.study.design.Factory.Singleton;
public class HungrySingleton {
/*静态实例在类加载时直接初始化创建*/
private static final HungrySingleton instance = new HungrySingleton();
/*构造方法私有化, 禁止外部进行创建该类实例*/
private HungrySingleton(){};
/*提供一个静态方法,返回唯一的实例对象*/
public static HungrySingleton getInstance(){
return instance;
}
}
饿汉模式适用于单例对象较少的情况。这样写可以保证绝对的线程安全,执行效率比较高。缺点是所有对象类在加载时就实例化,这样一来,如果系统中有大量的单例对象存在,在系统初始化时会造成系统内存浪费,导致系统内存不可控。也就是说,系统初始化时,无论用或者不用的单例对象都会初始化,并占用内存空间。可以通过懒汉模式解决。
懒汉模式
为了解决饿汉模式可能带来的内存浪费,于是出现了懒汉模式的写法。懒汉模式也就是延迟加载,只有单例对象在被使用时才会初始化。
懒汉模式需要解决线程安全的问题。在多线程环境下,在单例对象还未初始化,如果同时多个线程访问单例对象,会造成对象重复创建的情况,这就违背了单例的初衷,也可能会造成程序执行异常。
synchronize代码实现
package com.study.design.Factory.Singleton;
public class LazySynchronizeSingleton {
/*声明一个单例实例对象引用*/
private static LazySynchronizeSingleton instance= null;
/*私有化构造方法,禁止外部创建对象实例*/
private LazySynchronizeSingleton(){}
/**
* 通过 synchronized 同步锁保证线程安全
* 在使用单例对象的时候创建
* @return
*/
public synchronized static LazySynchronizeSingleton getInstance() {
if (instance == null) {
instance = new LazySynchronizeSingleton();
}
return instance;
}
}
synchronized 固然可以保证线程的安全,但是在线程数量剧增的情况下,会导致大批线程阻塞,从而导致程序性能下降。而且,单例对象只有在第一次使用的时候才需要加锁 ,一旦实例化之后,以后每次使用并不需要同步加锁。通过下面双重锁机制来优化
双重检查锁
package com.study.design.Factory.Singleton;
public class LazyDoubleCheckSingleton {
/**
* volatile 关键字修饰单例保证线程之间的可见性,在多个线程同时调用时正确处理 instance 变量
*/
private volatile static LazyDoubleCheckSingleton instance;
/*私有化构造方法, 禁止外部创建实例对象*/
private LazyDoubleCheckSingleton(){}
/*提供一个静态方法,返回唯一的实例对象*/
public static LazyDoubleCheckSingleton getInstance(){
/**
* 第一次 null 判断,多个线程可以同时执行此处,如果单例对象还未创建,只有一个会继续执行,而其他线程会阻塞
* 如果单例对象已经创建,则会直接返回实例对象
*/
if (instance == null) {
// 只有一个线程会获取到 synchronized 锁,其他线程阻塞在此处
synchronized (LazyDoubleCheckSingleton.class){
/**
* 第二次 null 判断,这里是避免对象重复创建的关键
* 早些被阻塞的线程,获取到锁之后, 会再次进行一次 null 判断
* 此时,实例对象应该是已经被创建了 instance == null 为 false,不会重新创建对象
*/
if (instance == null)
instance = new LazyDoubleCheckSingleton();
}
}
// 返回单例对象
return instance;
}
}
双重检查锁机制,解决了线程安全问题和性能问题。
volatile 关键字保证了线程之间的可见性,双重 null 判断解决了对象重复实例化的问题。但只要是使用 synchronized 关键字总是要上锁,对程序还是有一定影响的。我们可以从类初始化的角度考虑,采用静态内部类的方式来实现单例模式。
静态内部类
package com.study.design.Factory.Singleton;
public class LazyStaticInnerClassSingleton {
/*私有化构造方法,禁止外部创建实例对象*/
private LazyStaticInnerClassSingleton(){}
/*提供一个静态方法,返回唯一的实例对象*/
private LazyStaticInnerClassSingleton geInstance(){
return LazyHolder.INSTANCE;
}
/*默认是不加载内部类的*/
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
利用 Java 虚拟机对类实例化的机制,内部类只有在被使用的时候才会初始化。这种模式兼顾了饿汉模式写法的内存浪费和 synchronized 性能问题,实现也比较简单。
禁止反射机制破坏单例模式
以上三种写法,构造方法只是使用了 private 关键字修复,并无其他任何处理。如果是这样,我们仍然可以通过使用反射机制来调用其构造方法,从而创建另外一个实例对象 ,所以以上三种还不能保证绝对的单例。
我们需要在构造方法中加上一个判断,如果实例已经创建过,直接抛出异常:
if (LazyHolder.INSTANCE != null){
throw new RuntimeException("不允许创建多个实例");
}
关于序列化
对于一个单例模式的实例,在其被实例化后,重新被反序列化为一个对象的时候,应该与被序列化时的对象是同一个实例,这样才是更为严格的单例模式。
为了将上诉单例模式实现方法变成可序列化的,单纯的实现 Serrializble 接口,是不够的。必须声明所有的实例变量都是 transient 类型,并且提供一个 readResolve() 方法,否则每次反序列化时都会创建一个新的实例。
package com.study.design.Factory.Singleton;
import java.io.Serializable;
public class SerializableSingleton implements Serializable {
private transient volatile static SerializableSingleton instance;
/*私有化构造方法, 禁止外部创建实例对象*/
private SerializableSingleton(){}
/*提供一个静态方法,返回唯一的实例对象*/
public static SerializableSingleton getInstance(){
if (instance == null) {
// 只有一个线程会获取到 synchronized 锁,其他线程阻塞在此处
synchronized (LazyDoubleCheckSingleton.class){
if (instance == null)
instance = new SerializableSingleton();
}
}
// 返回单例对象
return instance;
}
private SerializableSingleton readResolve(){
return instance;
}
}
对于一个正在被反序列化的对象,如果它的类定义了一个 readResolve() 方法,并且具备正确的声明,那么在反序列化之后 ,新建对象上的 readResolve() 方法就会被调用,然后该方法返回的对象应用将会被返回,取代新建的对象。
但其实新对象还是被创建了的,只是创建之后没有任何引用会被垃圾回收器回收。
枚举
package com.study.design.Factory.Singleton;
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
这种试下方式更加简洁,无偿提供了序列化机制。可以绝对放置多次实例化,也可以防止反射机制创建新的对象。枚举类型写法是 Effective Java 一书中推荐的写法。
容器式单例写法
虽然枚举式单例写法更加优雅,但是也会存在一些问题,因为它在类加载时将所有对象初始化都放在类内存中,这其实和饿汉式写法并无差异,不适合大量单例对象的场景。通过使用容器式单例写法可以解决大规模生产单例的问题。
package com.study.design.Factory.Singleton;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 容器式单例模式
* 决大规模生产单例的问题
*/
public class ContainerSingleton {
/*定义一个容器*/
private static Map<String,Object> ioc = new ConcurrentHashMap<>();
/*构造方法私有化, 禁止外部进行创建该类实例*/
private ContainerSingleton(){}
/*提供一个静态方法,返回唯一的实例对象*/
public static Object getBean(String className){
synchronized (ioc){
if (!ioc.containsKey(className)){
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className,obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}else{
return ioc.get(className);
}
}
}
}