设计模式---单例模式

本文详细介绍了Java中的单例模式实现,包括懒汉模式、饿汉模式、静态内部类实现,并讨论了反射攻击和反序列化漏洞。针对这些问题,提出了防御措施,如在构造器中进行检查并抛出异常,以及重写`readResolve`方法来防止反序列化创建新实例。同时,提到了Java枚举类型的天然单例特性。最后,简述了多例模式和享元模式的区别以及它们在实际应用中的场景,如Spring框架中的单例管理。
摘要由CSDN通过智能技术生成

定义:保证一个类中只有一个实例,保证提供一个全局访问点。

单例模式的特点:

  • 类构造器私有
  • 持有自己类的引用
  • 对外提供获取实例的静态方法
1.懒汉模式

在这里插入图片描述
在这里插入图片描述

2.饿汉模式

在这里插入图片描述
在这里插入图片描述

3.静态内部类

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

3.2反射攻击

先回顾下反射获取运行时类的构造方法:

  • getConstructors()返回所有public的构造器。

  • getDeclaredConstructors()返回所有private和public构造器。

  • getConstructor()返回指定参数类型public的构造器。

  • getDeclaredConstructor()返回指定参数类型的private和public构造器。

反射获取运行时类的属性:

  • getFields()返回所有public的字段。

  • getDeclaredFields()返回所有private和public字段。

  • getField()返回指定字段名public的字段。

  • getDeclaredField()返回指定字段名的private和public字段名。

如果其他人使用反射,依然能够通过类的无参构造方式创建对象。例如:

Class<SimpleSingleton5> simpleSingleton5Class = SimpleSingleton5.class;
try {
    SimpleSingleton5 newInstance = simpleSingleton5Class.newInstance();
    System.out.println(newInstance == SimpleSingleton5.getInstance());
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

上面代码打印结果是false。由此看出,通过反射创建的对象,跟通过getInstance方法获取的对象,并非同一个对象,也就是说,这个漏洞会导致SimpleSingleton5非单例。

那么,要如何防止这个漏洞呢?
答:这就需要在无参构造方式中判断,如果非空,则抛出异常了。

public class SimpleSingleton5 {

    private SimpleSingleton5() {
        if(Inner.INSTANCE != null) {
           throw new RuntimeException("不能支持重复实例化");
       }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
        }
    }

}

3.3 反序列化漏洞

java中的类通过实现Serializable接口,可以实现序列化。我们可以把类的对象先保存到内存,或者某个文件当中。后面在某个时刻,再恢复成原始对象。

public class SimpleSingleton5 implements Serializable {

    private SimpleSingleton5() {
        if (Inner.INSTANCE != null) {
            throw new RuntimeException("不能支持重复实例化");
        }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }

    private static void writeFile() {
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        try {
            SimpleSingleton5 simpleSingleton5 = SimpleSingleton5.getInstance();
            fos = new FileOutputStream(new File("test.txt"));
            oos = new ObjectOutputStream(fos);
            oos.writeObject(simpleSingleton5);
            System.out.println(simpleSingleton5.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    private static void readFile() {
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        try {
            fis = new FileInputStream(new File("test.txt"));
            ois = new ObjectInputStream(fis);
            SimpleSingleton5 myObject = (SimpleSingleton5) ois.readObject();

            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        writeFile();
        readFile();
    }
}

运行之后,发现序列化和反序列化后对象的hashCode不一样:
189568618
793589513

说明,反序列化时创建了一个新对象,打破了单例模式对象唯一性的要求。

那么如何解决这个问题呢?
重新readResolve方法。做法很简单,只需要在readResolve方法中,每次都返回唯一的Inner.INSTANCE对象即可。

private Object readResolve() throws ObjectStreamException {
    return Inner.INSTANCE;
}

290658609
290658609
4. 枚举类型Enum

其实在java中枚举就是天然的单例,每一个实例只有一个对象,这是java底层内部机制保证的。

public enum  SimpleSingleton7 {
    INSTANCE;
    
    public void doSamething() {
        System.out.println("doSamething");
    }
}   

jvm保证了枚举是天然的单例,并且不存在线程安全问题,此外,还支持序列化。单元素的枚举类型已经成为实现Singleton的最佳方法。

5.多例模式

允许创建多个实例。但它的初衷是为了控制实例的个数,其他的跟单例模式差不多。

public class SimpleMultiPattern {
    //持有自己类的引用
    private static final SimpleMultiPattern INSTANCE1 = new SimpleMultiPattern();
    private static final SimpleMultiPattern INSTANCE2 = new SimpleMultiPattern();

    //私有的构造方法
    private SimpleMultiPattern() {
    }
    //对外提供获取实例的静态方法
    public static SimpleMultiPattern getInstance(int type) {
        if(type == 1) {
          return INSTANCE1;
        }
        return INSTANCE2;
    }
}

多例模式和享元模式有什么区别:

  • 多例模式:跟单例模式一样,纯粹是为了控制实例数量,使用这种模式的类,通常是作为程序某个模块的入口。
  • 享元模式:它的侧重点是对象之间的衔接。它把动态的、会变化的状态剥离出来,共享不变的东西。
6. 使用场景
Runtime

jdk提供了Runtime类,我们可以通过这个类获取系统的运行状态。比如可以通过它获取cpu核数:

int availableProcessors = Runtime.getRuntime().availableProcessors();

Runtime类的关键代码如下:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
    ...
}

从上面的代码我们可以看出,这是一个单例模式,并且是饿汉模式。

NamespaceHandlerResolver

spring提供的DefaultNamespaceHandlerResolver是为需要初始化默认命名空间处理器,是为了方便后面做标签解析用的。

Nullable
private volatile Map<String, Object> handlerMappings;

private Map<String, Object> getHandlerMappings() {
  Map<String, Object> handlerMappings = this.handlerMappings;
  if (handlerMappings == null) {
   synchronized (this) {
    handlerMappings = this.handlerMappings;
    if (handlerMappings == null) {
     if (logger.isDebugEnabled()) {
      logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
     }
     try {
      Properties mappings =
        PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
      if (logger.isDebugEnabled()) {
       logger.debug("Loaded NamespaceHandler mappings: " + mappings);
      }
      handlerMappings = new ConcurrentHashMap<>(mappings.size());
      CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
      this.handlerMappings = handlerMappings;
     }
     catch (IOException ex) {
      throw new IllegalStateException(
        "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
     }
    }
   }
  }
  return handlerMappings;
 }

我们看到它使用了双重检测锁,并且还定义了一个局部变量handlerMappings,这是非常高明之处。

使用局部变量相对于不使用局部变量,可以提高性能。主要是由于 volatile 变量创建对象时需要禁止指令重排序,需要一些额外的操作。

spring 的单例

以前在spring中要定义一个bean,需要在xml文件中做如下配置:

<bean id="test" class="com.susan.Test" init-method="init" scope="singleton">

bean标签上有个scope属性,我们可以通过指定该属性控制bean实例是单例的,还是多例的。如果值为singleton,代表是单例的。当然如果该参数不指定,默认也是单例的。如果值为prototype,则代表是多例的。

在spring的AbstractBeanFactory类的doGetBean方法中:

if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      return createBean(beanName, mbd, args);
  });
  bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
} else if (mbd.isPrototype()) {
    Object prototypeInstance = createBean(beanName, mbd, args);
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
} else {
    ....
}
  • 判断如果scope是singleton,则调用getSingleton方法获取实例。

  • 如果scope是prototype,则直接创建bean实例,每次会创建一个新实例。

  • 如果scope是其他值,则允许我们自定bean的创建过程。

其中getSingleton方法主要代码如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
  Assert.notNull(beanName, "Bean name must not be null");
  synchronized (this.singletonObjects) {
   Object singletonObject = this.singletonObjects.get(beanName);
   if (singletonObject == null) {
          singletonObject = singletonFactory.getObject();
         if (newSingleton) {
           addSingleton(beanName, singletonObject);
        }
   }
   return singletonObject;
  }
}

有个关键的singletonObjects对象,其实是一个ConcurrentHashMap集合:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
  • 根据beanName先从singletonObjects集合中获取bean实例。
  • 如果bean实例不为空,则直接返回该实例。
  • 如果bean实例为空,则通过getObject方法创建bean实例,然后通过addSingleton方法,将该bean实例添加到singletonObjects集合中。
  • 下次再通过beanName从singletonObjects集合中,就能获取到bean实例了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值