单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

一、饿汉式单例

  1. public class HangurySingleton {
  2. private static final HangurySingleton INSTANCE = new HangurySingleton();
  3. private HangurySingleton(){}
  4. public static HangurySingleton getInstance() {
  5. return INSTANCE;
  6. }
  7. }

饿汉式单例很简单,是在内部维护一个final static的对象,这个对象会在JVM启动时直接创建。之后在调用时使用getInstance()方法返回这个对象即可。

优点:简单,容易理解,不存在线程安全问题,因为对象在应用一启动时就被创建好了
缺点:容易造成资源的浪费

当然,上面代码还可以写成以下形式,在static代码块中初始化,效果与上面相同:

  1. public class HangurySingleton {
  2. private static final HangurySingleton INSTANCE;
  3. static {
  4. INSTANCE = new HangurySingleton();
  5. }
  6. private HangurySingleton(){}
  7. public static HangurySingleton getInstance() {
  8. return INSTANCE;
  9. }
  10. }

二、懒汉式单例

  1. public class LazySingleton {
  2. private static LazySingleton INSTANCE;
  3. private LazySingleton(){}
  4. public static LazySingleton getInstance() {
  5. if(INSTANCE == null) {
  6. INSTANCE = new LazySingleton();
  7. }
  8. return INSTANCE;
  9. }
  10. }

顾名思义,懒汉式单例就是在该对象第一次使用(调用getInstance()方法)时创建对象。
但是这就存在了一个问题,当多个线程同时调用getInstance()方法时,可能会出现对象不一致的情况,做以下改动:

  1. public class LazySingleton {
  2. private static LazySingleton INSTANCE;
  3. private LazySingleton(){}
  4. public synchronized static LazySingleton getInstance() {
  5. if(INSTANCE == null) {
  6. INSTANCE = new LazySingleton();
  7. }
  8. return INSTANCE;
  9. }
  10. }

直接在方法上加synchronized关键字给该对象加锁,多线程在访问该方法时,先进的线程会得到锁,后面的线程会进入等待状态,等待前面的线程释放锁后,依次进入方法。在实现单例上无疑是没有问题的,但是如果这个类还有其他方法,多线程状态下访问其他方法,也会被这个对象锁给一起锁上,所以需要做以下改动:

  1. public class LazySingleton {
  2. private static LazySingleton INSTANCE;
  3. private LazySingleton(){}
  4. public static LazySingleton getInstance() {
  5. if(INSTANCE == null) {
  6. synchronized (LazySingleton.class) {
  7. if(INSTANCE == null) {
  8. INSTANCE = new LazySingleton();
  9. }
  10. }
  11. }
  12. return INSTANCE;
  13. }
  14. }

给这段代码加上双重检验锁机制(Double Check),这样就结束了吗?并没有,在这段代码中,还可能会有指令重排序问题,我们需要加上volidate关键字:
private volatile static LazySingleton INSTANCE;

三、内部类实现单例

  1. public class InnerSingleton {
  2. private InnerSingleton(){}
  3. public static InnerSingleton getInstance() {
  4. return SingletonHolder.INSTANCE;
  5. }
  6. private static final class SingletonHolder {
  7. private static final InnerSingleton INSTANCE = new InnerSingleton();
  8. }
  9. }

这种方式实现单例,更为巧妙,利用的是内部类的加载机制,在第一次调用getInstance()方法时,会自动创建内部类final对象,从而实现单例。

(×)序列化与反序列化问题

在以上三种方式,都存在一个问题,就是在进行序列化和反序列化时,会破坏单例,我们以懒汉式单例为例,先看以下代码:

  1. public class SerializableTest {
  2. public static void main(String[] args) throws Exception{
  3. LazySingleton s1 = LazySingleton.getInstance();
  4. LazySingleton s2;
  5. //将对象写出至文件
  6. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.obj"));
  7. oos.writeObject(s1);
  8. oos.flush();
  9. oos.close();
  10. //从文件中读取对象
  11. ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Singleton.obj"));
  12. s2 = (LazySingleton) ois.readObject();
  13. ois.close();
  14. System.out.println(s1);
  15. System.out.println(s2);
  16. System.out.println(s1 == s2);
  17. }
  18. }

执行结果:

  1. com.github.lihongjie.javastudy.designpattern.singleton.lazy.LazySingleton@5e481248
  2. com.github.lihongjie.javastudy.designpattern.singleton.lazy.LazySingleton@1d81eb93
  3. false

会发现,将对象写出到文件后再从文件后,再从文件中读取,会使该对象重新创建,影响到单例的唯一性。解决这个问题也很简单,我们只需要重写readResolve()方法:

  1. import java.io.Serializable;
  2. public class LazySingleton implements Serializable {
  3. private volatile static LazySingleton INSTANCE;
  4. private LazySingleton(){}
  5. public static LazySingleton getInstance() {
  6. if(INSTANCE == null) {
  7. synchronized (LazySingleton.class) {
  8. if(INSTANCE == null) {
  9. INSTANCE = new LazySingleton();
  10. }
  11. }
  12. }
  13. return INSTANCE;
  14. }
  15. private Object readResolve() {
  16. return INSTANCE;
  17. }
  18. }

再执行上面的测试方法,问题已经解决啦!

  1. com.github.lihongjie.javastudy.designpattern.singleton.lazy.LazySingleton@5e481248
  2. com.github.lihongjie.javastudy.designpattern.singleton.lazy.LazySingleton@5e481248
  3. true

四、枚举类实现单例

在《Effective Java》一书中,为我们推荐了一种更加简单且高效的单例方式,就是用枚举类来实现:

  1. public enum EnumSingleton {
  2. INSTANCE;
  3. private EnumSingleton() {
  4. this.data = new Object();
  5. }
  6. private Object data;
  7. public Object getData() {
  8. return data;
  9. }
  10. }

这种使用枚举方式实现的单例,在面对复杂的序列化或者反射攻击时任然可以绝对防止多次实例化,被作者所推崇。
使用枚举类实现单例的原理如下:

  1. Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.EnumvalueOf()方法来根据名字查找枚举对象。也就是说,序列化的时候只将INSTANCE这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

五、容器式单例

  1. Spring IOC中,所有的bean对象交由IOC容器统一管理,需要取用时,会从IOC容器中取出该对象的实例,这里便使用到了容器式单例。<br /> 容器单例的思路是这样的,维护一个Map,把单例的实例都存放进该Map中,用的时候只从该Map中取,试图实现单例;但这种思路有局限性,Map中存的单例对象是可以被更新掉的,如果两次取的间隔,发生了单例对象的更新,就会取到2个不同的对象,破坏了单例性。但是如果程序中单例类很多,可以考虑用一个容器管理起来。
  1. import java.util.Map;
  2. import java.util.concurrent.ConcurrentHashMap;
  3. public class ContainerSingleton {
  4. private static final Map<String, Object> SINGLETON_MAP = new ConcurrentHashMap<>();
  5. private ContainerSingleton(){}
  6. public static void setInstance(String key, Object obj) {
  7. if(key != null && !key.isEmpty() && obj != null) {
  8. if(!SINGLETON_MAP.containsKey(key)) {
  9. SINGLETON_MAP.put(key, obj);
  10. }
  11. }
  12. }
  13. public static Object getInstance(String key) {
  14. return SINGLETON_MAP.get(key);
  15. }
  16. }

六、基于ThreadLocal的单例

  1. 不能保证程序全局唯一,但能保证线程唯一。以空间换时间,多线程下为每个线程提供实例。在一定场合下也有应用场景。
  1. public class ThreadLocalSingleton {
  2. public final static ThreadLocal<ThreadLocalSingleton> INSTANCE =
  3. new ThreadLocal<ThreadLocalSingleton>(){
  4. @Override
  5. protected ThreadLocalSingleton initialValue() {
  6. return new ThreadLocalSingleton();
  7. }
  8. };
  9. private ThreadLocalSingleton() {}
  10. public ThreadLocalSingleton getInstance() {
  11. return INSTANCE.get();
  12. }
  13. }

七、总结

单例模式重点:
1、私有化构造器
2、保证线程安全
3、延迟加载
4、防止序列化和反序列化破坏单例
5、防御反射攻击单例

单例模式缺点:
没有接口,扩展困难。如果要扩展单例模式,只有修改代码,没有其他途径。从某种程度上讲,不符合开闭原则。