单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
一、饿汉式单例
public class HangurySingleton {private static final HangurySingleton INSTANCE = new HangurySingleton();private HangurySingleton(){}public static HangurySingleton getInstance() {return INSTANCE;}}
饿汉式单例很简单,是在内部维护一个final static的对象,这个对象会在JVM启动时直接创建。之后在调用时使用getInstance()方法返回这个对象即可。
优点:简单,容易理解,不存在线程安全问题,因为对象在应用一启动时就被创建好了
缺点:容易造成资源的浪费
当然,上面代码还可以写成以下形式,在static代码块中初始化,效果与上面相同:
public class HangurySingleton {private static final HangurySingleton INSTANCE;static {INSTANCE = new HangurySingleton();}private HangurySingleton(){}public static HangurySingleton getInstance() {return INSTANCE;}}
二、懒汉式单例
public class LazySingleton {private static LazySingleton INSTANCE;private LazySingleton(){}public static LazySingleton getInstance() {if(INSTANCE == null) {INSTANCE = new LazySingleton();}return INSTANCE;}}
顾名思义,懒汉式单例就是在该对象第一次使用(调用getInstance()方法)时创建对象。
但是这就存在了一个问题,当多个线程同时调用getInstance()方法时,可能会出现对象不一致的情况,做以下改动:
public class LazySingleton {private static LazySingleton INSTANCE;private LazySingleton(){}public synchronized static LazySingleton getInstance() {if(INSTANCE == null) {INSTANCE = new LazySingleton();}return INSTANCE;}}
直接在方法上加synchronized关键字给该对象加锁,多线程在访问该方法时,先进的线程会得到锁,后面的线程会进入等待状态,等待前面的线程释放锁后,依次进入方法。在实现单例上无疑是没有问题的,但是如果这个类还有其他方法,多线程状态下访问其他方法,也会被这个对象锁给一起锁上,所以需要做以下改动:
public class LazySingleton {private static LazySingleton INSTANCE;private LazySingleton(){}public static LazySingleton getInstance() {if(INSTANCE == null) {synchronized (LazySingleton.class) {if(INSTANCE == null) {INSTANCE = new LazySingleton();}}}return INSTANCE;}}
给这段代码加上双重检验锁机制(Double Check),这样就结束了吗?并没有,在这段代码中,还可能会有指令重排序问题,我们需要加上volidate关键字:private volatile static LazySingleton INSTANCE;
三、内部类实现单例
public class InnerSingleton {private InnerSingleton(){}public static InnerSingleton getInstance() {return SingletonHolder.INSTANCE;}private static final class SingletonHolder {private static final InnerSingleton INSTANCE = new InnerSingleton();}}
这种方式实现单例,更为巧妙,利用的是内部类的加载机制,在第一次调用getInstance()方法时,会自动创建内部类final对象,从而实现单例。
(×)序列化与反序列化问题
在以上三种方式,都存在一个问题,就是在进行序列化和反序列化时,会破坏单例,我们以懒汉式单例为例,先看以下代码:
public class SerializableTest {public static void main(String[] args) throws Exception{LazySingleton s1 = LazySingleton.getInstance();LazySingleton s2;//将对象写出至文件ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.obj"));oos.writeObject(s1);oos.flush();oos.close();//从文件中读取对象ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Singleton.obj"));s2 = (LazySingleton) ois.readObject();ois.close();System.out.println(s1);System.out.println(s2);System.out.println(s1 == s2);}}
执行结果:
com.github.lihongjie.javastudy.designpattern.singleton.lazy.LazySingleton@5e481248com.github.lihongjie.javastudy.designpattern.singleton.lazy.LazySingleton@1d81eb93false
会发现,将对象写出到文件后再从文件后,再从文件中读取,会使该对象重新创建,影响到单例的唯一性。解决这个问题也很简单,我们只需要重写readResolve()方法:
import java.io.Serializable;public class LazySingleton implements Serializable {private volatile static LazySingleton INSTANCE;private LazySingleton(){}public static LazySingleton getInstance() {if(INSTANCE == null) {synchronized (LazySingleton.class) {if(INSTANCE == null) {INSTANCE = new LazySingleton();}}}return INSTANCE;}private Object readResolve() {return INSTANCE;}}
再执行上面的测试方法,问题已经解决啦!
com.github.lihongjie.javastudy.designpattern.singleton.lazy.LazySingleton@5e481248com.github.lihongjie.javastudy.designpattern.singleton.lazy.LazySingleton@5e481248true
四、枚举类实现单例
在《Effective Java》一书中,为我们推荐了一种更加简单且高效的单例方式,就是用枚举类来实现:
public enum EnumSingleton {INSTANCE;private EnumSingleton() {this.data = new Object();}private Object data;public Object getData() {return data;}}
这种使用枚举方式实现的单例,在面对复杂的序列化或者反射攻击时任然可以绝对防止多次实例化,被作者所推崇。
使用枚举类实现单例的原理如下:
Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。也就是说,序列化的时候只将INSTANCE这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
五、容器式单例
在Spring IOC中,所有的bean对象交由IOC容器统一管理,需要取用时,会从IOC容器中取出该对象的实例,这里便使用到了容器式单例。<br /> 容器单例的思路是这样的,维护一个Map,把单例的实例都存放进该Map中,用的时候只从该Map中取,试图实现单例;但这种思路有局限性,Map中存的单例对象是可以被更新掉的,如果两次取的间隔,发生了单例对象的更新,就会取到2个不同的对象,破坏了单例性。但是如果程序中单例类很多,可以考虑用一个容器管理起来。
import java.util.Map;import java.util.concurrent.ConcurrentHashMap;public class ContainerSingleton {private static final Map<String, Object> SINGLETON_MAP = new ConcurrentHashMap<>();private ContainerSingleton(){}public static void setInstance(String key, Object obj) {if(key != null && !key.isEmpty() && obj != null) {if(!SINGLETON_MAP.containsKey(key)) {SINGLETON_MAP.put(key, obj);}}}public static Object getInstance(String key) {return SINGLETON_MAP.get(key);}}
六、基于ThreadLocal的单例
不能保证程序全局唯一,但能保证线程唯一。以空间换时间,多线程下为每个线程提供实例。在一定场合下也有应用场景。
public class ThreadLocalSingleton {public final static ThreadLocal<ThreadLocalSingleton> INSTANCE =new ThreadLocal<ThreadLocalSingleton>(){@Overrideprotected ThreadLocalSingleton initialValue() {return new ThreadLocalSingleton();}};private ThreadLocalSingleton() {}public ThreadLocalSingleton getInstance() {return INSTANCE.get();}}
七、总结
单例模式重点:
1、私有化构造器
2、保证线程安全
3、延迟加载
4、防止序列化和反序列化破坏单例
5、防御反射攻击单例
单例模式缺点:
没有接口,扩展困难。如果要扩展单例模式,只有修改代码,没有其他途径。从某种程度上讲,不符合开闭原则。
