定义:保证一个类中只有一个实例,保证提供一个全局访问点。
单例模式的特点:
- 类构造器私有
- 持有自己类的引用
- 对外提供获取实例的静态方法
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实例了。