Java设计模式——单例模式

概述

一个类在全局只需要一个实例,例如线程池,缓存注册或者配置对象。

实现方式

  1. 饿汉式
  2. 懒汉式
  3. 注册式
  4. 静态内部类
  5. 枚举式

饿汉式单例

  1. /**
  2. * 饿汉式单例,它是在类加载的时候就立即初始化,并且创建单例对象
  3. * 优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好
  4. * 缺点:类加载的时候就初始化,不管你用还是不用,我都占着空间
  5. * 浪费了内存,有可能占着茅坑不拉屎
  6. * 绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题
  7. *
  8. * CountDownLatch是一个计数器,它使得一个线程等待其他各自线程执行完毕之后再执行
  9. * 线程完成一个记录一个,计数器递减,当计数器值为0时,表示所有线程执行完成,在闭锁上
  10. * 等待的线程就可以恢复工作了
  11. */
  12. public class HungerSingleton {
  13. private HungerSingleton(){}
  14. private static HungerSingleton singleton=new HungerSingleton();
  15. public static HungerSingleton getInstance(){
  16. return singleton;
  17. }
  18. public static class ThreadTest implements Runnable{
  19. private static CountDownLatch countDownLatch;
  20. public ThreadTest(CountDownLatch countDownLatch) {
  21. this.countDownLatch = countDownLatch;
  22. }
  23. @Override
  24. public void run() {
  25. try {
  26. countDownLatch.await();
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. HungerSingleton hungrySingleton = HungerSingleton.getInstance();
  31. System.out.println(Thread.currentThread().getName() +":"+hungrySingleton);
  32. }
  33. }
  34. public static void main(String[] args) {
  35. //CountDownLatch(int count)唯一构造器
  36. CountDownLatch cdl = new CountDownLatch(1000);
  37. for(int i=0;i<1000;i++){
  38. Thread t = new Thread(new HungrySingleton.TestThread(cdl),i+"");
  39. t.start();
  40. cdl.countDown();
  41. }
  42. }
  43. }

懒汉式单例

实现方式一
  1. /**懒汉可以理解为延迟加载,相比于用静态变量的方式,只有在使用的时候才会初始化对象
  2. *
  3. * 这种写法非线程安全
  4. */
  5. public class LazySingleton {
  6. private LazySingleton() {
  7. }
  8. private static LazySingleton lazySingleton = null;
  9. public static LazySingleton getInstance(){
  10. //会存在线程安全问题,假设27行 new的过程需要5s,并发情况下代码在26行判断的时候是null,一起阻塞在27行,5s后产生大量对象
  11. if(null==lazySingleton){
  12. lazySingleton = new LazySingleton();
  13. }
  14. return lazySingleton ;
  15. }
  16. public static class TestThread2 implements Runnable{
  17. private static CountDownLatch cd;
  18. public TestThread2(CountDownLatch countDownLatch) {
  19. this.cd = countDownLatch;
  20. }
  21. @Override
  22. public void run() {
  23. try {
  24. cd.await();
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. LazySingleton lazySingleton = LazySingleton.getInstance();
  29. System.out.println(Thread.currentThread().getName() +":"+lazySingleton);
  30. }
  31. }
  32. public static void main(String[] args) {
  33. CountDownLatch cdl = new CountDownLatch(100);
  34. for(int i=0;i<100;i++){
  35. Thread t = new Thread(new TestThread2(cdl),i+"");
  36. t.start();
  37. cdl.countDown();
  38. }
  39. }
  40. }

实现方式二(Synchronized修饰方法)
  1. public class LazySingletonSynchronized {
  2. private LazySingletonSynchronized() {
  3. }
  4. //volatile保证多线程间的可见性,防止A线程创建对象后没有即时通知到B线程,B线程重现创建
  5. private static LazySingletonSynchronized lazySingleton = null;
  6. /**
  7. * 这个方法简单粗暴,保证了线程安全,达到了延迟加载的目的,但是存在的问题是每次进行getInstance的时候需要加锁,性能不是很好
  8. * @return
  9. */
  10. public static synchronized LazySingletonSynchronized getInstance(){
  11. if(null==lazySingleton){
  12. lazySingleton = new LazySingletonSynchronized();
  13. }
  14. return lazySingleton ;
  15. }
  16. public static class TestThread3 implements Runnable{
  17. private static CountDownLatch cd;
  18. public TestThread3(CountDownLatch countDownLatch) {
  19. this.cd = countDownLatch;
  20. }
  21. @Override
  22. public void run() {
  23. try {
  24. cd.await();
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. }
  28. LazySingletonSynchronized lazySingleton = LazySingletonSynchronized.getInstance();
  29. System.out.println(Thread.currentThread().getName() +":"+lazySingleton);
  30. }
  31. }

分析

线程安全,懒加载。但是由于方法加锁,多线程的情况下性能不好。

仔细分析使用场景,发现第一次创建类对象的时候给方法加锁是对的,由于锁是加在方法上,当创建完对象后,每次获取实例仍然进行了加锁操作,这是可以优化的地方,所以有了下面的实现方式三

实现方法三(double check)
  1. public class DoubleCheckSingleton {
  2. private static volatile DoubleCheckSingleton doubleCheckSingleton ;
  3. private DoubleCheckSingleton(){}
  4. /**
  5. * 优点:实现了延时加载,性能更佳,锁粒度更小,减少竞争。因为大部分情况第一次null判断返回的是false,就不需要进入锁了
  6. * @return
  7. */
  8. public static DoubleCheckSingleton getInstance(){
  9. if( null==doubleCheckSingleton){
  10. synchronized (DoubleCheckSingleton.class){
  11. if(null==doubleCheckSingleton){
  12. doubleCheckSingleton = new DoubleCheckSingleton();
  13. }
  14. }
  15. }
  16. return doubleCheckSingleton;
  17. }
  18. }

分析

synchronized如何保证可见性?原来获取锁的线程在释放锁之前会把工作内存中修改过的变量同步到主内存中,当其他线程重新获取同一个锁之后会重新获取主内存中的数据到自己的工作内存中去操作。

  • 第一个null判断作用当对象创建完成后,读取对象的时候直接返回已经创建好的对象,优化了性能
  • 第二个null判断是避免当前线程获取到锁之后,对象已经被上一个获取到锁的线程创建完。
  • 声明对象的时候要用volatile修饰。假设A,B线程都从主内存拷贝了一份到自己的线程中操作,A线程创建了对象,如果不用volatile修饰,B线程是不可见的,会导致创建多个实例

注册式单例

  1. /**
  2. * 将对象的class作为key,实例对象作为value放入一个线程安全的集合容器中,
  3. * 容器中没有则利用反射创建实例,否则从容器中获取已经创建的实例
  4. */
  5. public class SingletonManager {
  6. //线程安全的容器,饿汉式保证容器对象本身为单例
  7. private static Map map = new ConcurrentHashMap();
  8. //外部访问点,传入类名,返回该类的单例对象.该类会被登记进入上面的容器进行单例管理
  9. //在类中务必保证构造方法私有化,对这一点这个管理类是无法控制的,需要自己保证
  10. public static Object getInstance(String className) {
  11. //如果还没登记到容器
  12. if (!map.containsKey(className)) {
  13. //用反射的方式创建对象(因为已经构造函数私有化),并登记到容器中
  14. try {
  15. map.put(className, Class.forName(className).newInstance());
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. //从容器中获取管理的单例对象并返回
  21. return map.get(className);
  22. }
  23. }

静态内部类

  1. public class InnerClassSingleton implements Serializable {
  2. private InnerClassSingleton() {
  3. }
  4. private static class InnerClassSingletonHolder{
  5. private static InnerClassSingleton innerClassSingleton = new InnerClassSingleton();
  6. }
  7. public static InnerClassSingleton getInstance(){
  8. return InnerClassSingletonHolder.innerClassSingleton;
  9. }
  10. }

实现单例需要注意的点

为了实现单例模式,其核心就是确保单例对象的唯一性

  • 无法通过new来随意创建对象,构造函数为private
  • 提供获取唯一实例对象的方法,通常是getInstance
  • 多线程并发的情况下保证唯一
  • 避免反射创建单例对象(反射攻击)
  • 避免通过序列化创建单例对象(如果单例类实现了Serializable)(序列化攻击)

由此可以利用Enum的天然属性,可以有效的保证上面的几个关键点。它属于饿汉模式的单例实现

  1. /**
  2. * 使用枚举实现单例。
  3. */
  4. public enum EnumSingleton {
  5. INSTANCE; // 唯一的实例对象
  6. public static EnumSingleton getInstance() {
  7. return INSTANCE;
  8. }
  9. // 单例对象的属性对象
  10. private Object obj = new Object();
  11. public Object getObj() {
  12. return obj;
  13. }
  14. /**
  15. * 单例提供的对外服务。
  16. */
  17. public Object getFactoryService() {
  18. return new Object();
  19. }
  20. }

上述代码的反编译代码

  1. public final class EnumSingleton extends Enum
  2. {
  3. public static EnumSingleton[] values()
  4. {
  5. return (EnumSingleton[])$VALUES.clone();
  6. }
  7. public static EnumSingleton valueOf(String name)
  8. {
  9. return (EnumSingleton)Enum.valueOf(com/ws/pattern/singleton/EnumSingleton, name);
  10. }
  11. // 无法通过new来随意创建对象,构造函数为private.
  12. private EnumSingleton(String s, int i)
  13. {
  14. super(s, i);
  15. obj = new Object();
  16. }
  17. // 提供获取唯一实例对象的方法,通常是getInstance
  18. public static EnumSingleton getInstance()
  19. {
  20. return INSTANCE;
  21. }
  22. public Object getObj()
  23. {
  24. return obj;
  25. }
  26. public Object getFactoryService()
  27. {
  28. return new Object();
  29. }
  30. // 提供获取唯一实例对象的方法,通常是getInstance
  31. // 也可以直接获取到INSTANCE,但是获取到的都是一个对象
  32. public static final EnumSingleton INSTANCE;
  33. private Object obj;
  34. private static final EnumSingleton $VALUES[];
  35. // 静态代码中实例化对象,多线程并发的情况下保证唯一,属于饿汉模式
  36. static
  37. {
  38. INSTANCE = new EnumSingleton("INSTANCE", 0);
  39. $VALUES = (new EnumSingleton[] {
  40. INSTANCE
  41. });
  42. }
  43. }

从反编译代码中我们可以看到Enum的本质:

  • 枚举本质上是个final类
  • 定义的枚举值实际上就是一个枚举类的不可变对象(比如这里的INSTANCE)
  • 在Enum类加载的时候,就已经实例化了这个对象
  • 无法通过new来创建枚举对象

Enum实现单例模式的几个关键点验证

1.避免反射创建单例对象(反射攻击)

  1. /**
  2. * 反射攻击。
  3. * 由于Enum天然的不允许反射创建实例,所以可以完美的防范反射攻击。
  4. */
  5. private static void reflectionAttack() {
  6. System.out.println("反射攻击单例对象-----------开始");
  7. try {
  8. Constructor con = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
  9. con.setAccessible(true);
  10. Object obj = con.newInstance("INSTANCE", 0); // 反射新建对象以破坏单例
  11. System.out.println(obj);
  12. System.out.println(EnumSingleton.getInstance());
  13. } catch (Exception e) {
  14. e.printStackTrace();
  15. }
  16. System.out.println("反射攻击单例对象-----------结束");
  17. }

分析

代码运行后,会抛出异常"java.lang.IllegalArgumentException: Cannot reflectively create enum objects"

  1. Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
  2. at java.lang.reflect.Constructor.newInstance(Constructor.java:416)
  3. at com.ws.pattern.singleton.EnumSingletonAppMain.reflectionAttack(EnumSingletonAppMain.java:43)
  4. at com.ws.pattern.singleton.EnumSingletonAppMain.main(EnumSingletonAppMain.java:9)

从异常可以看出来,newInstance抛出了异常。推测Java反射是不允许创建Enum对象的,看看源码Constructor.java中的newInstance方法,存在处理Enum类型实例化的一行判断代码

if ((clazz.getModifiers() & Modifier.ENUM) != 0)

满足这个条件就抛出异常。newInstance的JDK代码如下:

  1. public T newInstance(Object ... initargs)
  2. throws InstantiationException, IllegalAccessException,
  3. IllegalArgumentException, InvocationTargetException
  4. {
  5. if (!override) {
  6. if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
  7. Class<?> caller = Reflection.getCallerClass();
  8. checkAccess(caller, clazz, null, modifiers);
  9. }
  10. }
  11. if ((clazz.getModifiers() & Modifier.ENUM) != 0)
  12. throw new IllegalArgumentException("Cannot reflectively create enum objects");
  13. ConstructorAccessor ca = constructorAccessor; // read volatile
  14. if (ca == null) {
  15. ca = acquireConstructorAccessor();
  16. }
  17. @SuppressWarnings("unchecked")
  18. T inst = (T) ca.newInstance(initargs);
  19. return inst;
  20. }

原来反射机制不允许实例化Enum类型的对象,自然挡住了反射攻击。

2.避免通过序列化创建单例对象(如果单例类实现了Serializable)(序列化攻击)

  1. /**序列化对象后,如果执行反序列化,也可以创建一个对象。利用此机制来尝试创建一个新的Enum对象
  2. * 序列化攻击
  3. * 需要在单例类中增加read
  4. */
  5. private static void serializableAttack() {
  6. System.out.println("序列化攻击单例对象-----------开始");
  7. EnumSingleton singleton = EnumSingleton.getInstance();
  8. System.out.println(singleton);
  9. try {
  10. ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./EnumSingleton.out"));
  11. oos.writeObject(singleton);
  12. ObjectInputStream ois = new ObjectInputStream((new FileInputStream("./EnumSingleton.out")));
  13. Object obj = ois.readObject(); // 这里利用反序列化创建对象
  14. System.out.println(obj);
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. } catch (ClassNotFoundException e) {
  18. e.printStackTrace();
  19. }
  20. System.out.println("序列化攻击单例对象-----------结束");
  21. }

跟踪进入ois.readObject(),会进入ObjectInputStream.readObject0方法。其中会解析class的二进制,根据class的文件定义,分别解析不同类型的字段。重点关注case TC_ENUM如下所示:

  1. switch (tc) {
  2. case TC_NULL:
  3. return readNull();
  4. case TC_REFERENCE:
  5. return readHandle(unshared);
  6. case TC_CLASS:
  7. return readClass(unshared);
  8. case TC_CLASSDESC:
  9. case TC_PROXYCLASSDESC:
  10. return readClassDesc(unshared);
  11. case TC_STRING:
  12. case TC_LONGSTRING:
  13. return checkResolve(readString(unshared));
  14. case TC_ARRAY:
  15. return checkResolve(readArray(unshared));
  16. case TC_ENUM:
  17. return checkResolve(readEnum(unshared));
  18. case TC_OBJECT:
  19. return checkResolve(readOrdinaryObject(unshared));
  20. case TC_EXCEPTION:
  21. IOException ex = readFatalException();
  22. throw new WriteAbortedException("writing aborted", ex);
  23. case TC_BLOCKDATA:
  24. case TC_BLOCKDATALONG:
  25. if (oldMode) {
  26. bin.setBlockDataMode(true);
  27. bin.peek(); // force header read
  28. throw new OptionalDataException(
  29. bin.currentBlockRemaining());
  30. } else {
  31. throw new StreamCorruptedException(
  32. "unexpected block data");
  33. }
  34. case TC_ENDBLOCKDATA:
  35. if (oldMode) {
  36. throw new OptionalDataException(true);
  37. } else {
  38. throw new StreamCorruptedException(
  39. "unexpected end of block data");
  40. }
  41. default:
  42. throw new StreamCorruptedException(
  43. String.format("invalid type code: %02X", tc));
  44. }

进入readEnum方法,重点关注Enum.valueOf方法。如下所示:

  1. /**
  2. * Reads in and returns enum constant, or null if enum type is
  3. * unresolvable. Sets passHandle to enum constant's assigned handle.
  4. */
  5. private Enum<?> readEnum(boolean unshared) throws IOException {
  6. if (bin.readByte() != TC_ENUM) {
  7. throw new InternalError();
  8. }
  9. ObjectStreamClass desc = readClassDesc(false);
  10. if (!desc.isEnum()) {
  11. throw new InvalidClassException("non-enum class: " + desc);
  12. }
  13. int enumHandle = handles.assign(unshared ? unsharedMarker : null);
  14. ClassNotFoundException resolveEx = desc.getResolveException();
  15. if (resolveEx != null) {
  16. handles.markException(enumHandle, resolveEx);
  17. }
  18. String name = readString(false);
  19. Enum<?> result = null;
  20. Class<?> cl = desc.forClass();
  21. if (cl != null) {
  22. try {
  23. @SuppressWarnings("unchecked")
  24. Enum<?> en = Enum.valueOf((Class)cl, name); // 这里根据name和class拿到Enum实例。这里的name="INSTANCE"
  25. result = en;
  26. } catch (IllegalArgumentException ex) {
  27. throw (IOException) new InvalidObjectException(
  28. "enum constant " + name + " does not exist in " +
  29. cl).initCause(ex);
  30. }
  31. if (!unshared) {
  32. handles.setObject(enumHandle, result);
  33. }
  34. }
  35. handles.finish(enumHandle);
  36. passHandle = enumHandle;
  37. return result;
  38. }

再跟进Enum.valueOf方法。代码如下:

  1. /**
  2. * Returns the enum constant of the specified enum type with the
  3. * specified name. The name must match exactly an identifier used
  4. * to declare an enum constant in this type. (Extraneous whitespace
  5. * characters are not permitted.)
  6. *
  7. * <p>Note that for a particular enum type {@code T}, the
  8. * implicitly declared {@code public static T valueOf(String)}
  9. * method on that enum may be used instead of this method to map
  10. * from a name to the corresponding enum constant. All the
  11. * constants of an enum type can be obtained by calling the
  12. * implicit {@code public static T[] values()} method of that
  13. * type.
  14. *
  15. * @param <T> The enum type whose constant is to be returned
  16. * @param enumType the {@code Class} object of the enum type from which
  17. * to return a constant
  18. * @param name the name of the constant to return
  19. * @return the enum constant of the specified enum type with the
  20. * specified name
  21. * @throws IllegalArgumentException if the specified enum type has
  22. * no constant with the specified name, or the specified
  23. * class object does not represent an enum type
  24. * @throws NullPointerException if {@code enumType} or {@code name}
  25. * is null
  26. * @since 1.5
  27. */
  28. public static <T extends Enum<T>> T valueOf(Class<T> enumType,
  29. String name) {
  30. // 从enumConstantDirectory()中根据name获取对象
  31. T result = enumType.enumConstantDirectory().get(name);
  32. if (result != null)
  33. return result;
  34. if (name == null)
  35. throw new NullPointerException("Name is null");
  36. throw new IllegalArgumentException(
  37. "No enum constant " + enumType.getCanonicalName() + "." + name);
  38. }

enumConstantDirectory()是Class的方法,其本质是从Class.java的enumConstantDirectory属性中获取。代码如下:

  1. private volatile transient Map<String, T> enumConstantDirectory = null;

也就是说,Enum中定义的Enum成员值都被缓存在了这个Map中,Key是成员名称(比如“INSTANCE”),Value就是Enum的成员对象。这样的机制天然保证了取到的Enum对象是唯一的。即使是反序列化,也是一样的。

结论

经过上面的分析,枚举的实现天然地支持了单例模式的特点,大大降低了单例的开发难度。