Java 设计模式 - 单例

顾名思义,单例就是程序内部只有一个实例。java 单例包含 2 个要点:

  1. 构造方法私有。只能自己创建实例
  2. 公开静态访问方法。既然别人不能创建实例了,那么要获取实例,就得提供一个公开的静态方法了

常用写法

1 饿汉式

public class SingletonA {

  private static SingletonA instance = new SingletonA();

  private SingletonA() {
  }

  public static SingletonA getInstance() {
    return instance;
  }
}

这种方式在类装载的时候就初始化了实例,所以是线程安全的。

2 懒汉式

public class SingletonB {

  private static SingletonB instance = null;

  private SingletonB() {
  }

  public static SingletonB getInstance() {
    if (instance == null) {
      instance = new SingletonB();
    }
    return instance;
  }
}

这种方式在第一次访问 getInstance() 方法时才初始化实例,所以可以达到懒加载的效果,但是在高并发的情况下,多个线程有可能同时判断到 instance == nulltrue ,导致实例被初始化多次。为了实现线程安全,我们很容易想到给程序加锁,于是有个下面两种写法:

  1. 方法上加锁

    public class SingletonC {
    
      private static volatile SingletonC instance = null;
    
      private SingletonC() {
      }
    
      public static synchronized SingletonC getInstance() {
        if (instance == null) {
          instance = new SingletonC();
        }
        return instance;
      }
    }
    
  2. 代码块加锁

    public class SingletonD {
    
      private static volatile SingletonD instance = null;
      private static final Object lockObj = new Object();
    
      private SingletonD() {
      }
    
      public static SingletonD getInstance() {
        synchronized (lockObj) {
          if (instance == null) {
            instance = new SingletonD();
          }
        }
        return instance;
      }
    }
    

方法上加锁代码块加锁的方式都能实现线程安全,但每次访问都需要上锁,高并发情况下势必会影响程序性能,因此我们有了下面的改良版的加锁方式,即双重检查锁的实现:

public class SingletonE {

  private static volatile SingletonE instance = null;
  private static final Object lockObj = new Object();

  private SingletonE() {
  }

  public static SingletonE getInstance() {
    if (instance == null) {
      synchronized (lockObj) {
        if (instance == null) {
          instance = new SingletonE();
        }
      }
    }
    return instance;
  }
}

这种方式在初始化代码上加了 synchronized 同步锁,保证了高并发情况下只有一个线程执行了初始化操作,实现了线程安全,另外在初始化完成后,外层的 instance == null 将返回 false ,程序不会再次进入同步代码块,解决了访问性能问题。

3 静态内部类

public class SingletonF {

  private SingletonF() {
  }

  private static class SingletonHolder {

    static final SingletonF instance = new SingletonF();
  }

  public static SingletonF getInstance() {
    return SingletonHolder.instance;
  }
}

这种方式由 jvm 保证了单例的线程安全,在 getInstance() 第一次被调用时,内部类 SingletonHolder 被装载,装载过程中会初始化它的静态域,从而 SingletonF 被实例化,这种写法利用了 jvm 及内部类的特性实现了一个线程安全的单例模式,不需要加锁,同时也保证了性能,懒加载的情况个人推荐这种写法。

4 枚举

public enum SingletonG {

  instance;

  public static SingletonG getInstance() {
    return instance;
  }
}

通过 getInstance() 或者直接使用枚举 SingletonG.instance 来获取实例,枚举同样是在类装载时就初始化了,所以不是懒加载,也不存在线程安全问题。

5 线程内单例

public class SingletonH {

  private static ThreadLocal<SingletonH> instanceLocal = new ThreadLocal<SingletonH>();

  private int status;

  private SingletonH() {
    status = (int) (Math.random() * 100);
  }

  public static SingletonH getInstance() {
    SingletonH instance = instanceLocal.get();
    if (instance == null) {
      instance = new SingletonH();
      instanceLocal.set(instance);
    }
    return instance;
  }

  public int getStatus() {
    return status;
  }
}

这种方式可以保证 SingletonH 在每个线程内部是单例的,下面我们写段测试代码来验证下:

  @Test
  public void testSingleton() {
    ExecutorService pool = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 100; i++) {
      pool.submit(new Runnable() {
        @Override
        public void run() {
          long id = Thread.currentThread().getId();
          String name = Thread.currentThread().getName();
          SingletonH instance = SingletonH.getInstance();
          int status = instance.getStatus();
          System.out.println(String.format("thread[id=%s,name=%s],instance.status=%s", id, name, status));
        }
      });
      if (i % 5 == 0) {
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }
  }
}

在测试代码中创建一个5个线程数的线程池并向线程池提交 100 个任务,在任务内部打印线程号和 SingletonH 实例,通过控制台输出我们可以看出 instance.ststus 在每个线程内都是相等的。

总结

单例的写法有很多种,但大致都有上面几种方法衍变而来,应该使用哪种方式,要看具体的应用场景,就个人喜好而言,不需要懒加载的情况使用 SingletonA 饿汉式的写法,需要懒加载的情况使用 SingletonF 静态内部类的写法。