什么是单例模式
保证一个类仅有一个实例,并提供一个全局访问点。
单例优点
- 在内存中只有一个实例,减少内存开销;
- 可以避免对资源的多重占用;
- 设置全局访问点,严格控制访问(外部不能被new出来,只能使用提供的方法);
单例缺点
- 构造器是私有的
- 线程安全
- 延迟加载(在使用时才加载)
- 序列化和反序列化安全
- 反射
单例模式的几种写法
懒汉模式
注重延迟加载,在使用时才进行加载1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25/**
* @author kokio
* @create 2019-03-21 15:45
*/
public class LazySingleton {
//懒汉式 线程不安全
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
//线程安全 将LazySingleton方法改写;这样锁的是整个class,开销大,影响性能
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
DoubleCheck双重检查,兼顾性能和线程安全
1 | public class LazyDoubleCheckSingleton { |
静态内部类的方式
通过Class对象的初始化锁,保证在对象创建过程中,其他线程不会造成影响,只能等待Class对象的锁(静态内部类的初始化锁)1
2
3
4
5
6
7
8
9
10
11
12public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
}
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
}
饿汉式
1 | public class HungrySingleton { |
优点:写法简单,类加载时就进行了初始化,避免了线程同步问题。
缺点:浪费内存。
枚举单例
1 | public enum EnumInstance { |
枚举类序列化 反序列化都是同一个对象,不受序列化的破坏;另外也不能反射攻击1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
EnumInstance instance = EnumInstance.getInstance();
instance.setData(new Object());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumInstance newInstance = (EnumInstance) ois.readObject();
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(newInstance.getData() == instance.getData());
}
返回true
通过jad工具反编译Enum类 ,可以看到主类是final的,不能被继承,构造器为私有构造器,成员变量也是final的,而在类加载时就初始化了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45public final class EnumInstance extends Enum
{
public static EnumInstance[] values()
{
return (EnumInstance[])$VALUES.clone();
}
public static EnumInstance valueOf(String name)
{
return (EnumInstance)Enum.valueOf(cn/footman/design/pattern/creational/singleton/EnumInstance, name);
}
private EnumInstance(String s, int i)
{
super(s, i);
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
public static EnumInstance getInstance()
{
return INSTANCE;
}
public static final EnumInstance INSTANCE;
private Object data;
private static final EnumInstance $VALUES[];
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
}
基于容器的单例模式
这样是线程不安全的,可以改成hashtable,线程安全,但是影响性能,不可取。根据场景使用:主要是能够统一管理。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class ContainerSingleton {
private static Map<String,Object> singletonMap = new HashMap<>();
public static void putInstance(String key,Object instance){
if(StringUtils.isNotBlank(key) && instance != null){
if(!singletonMap.containsKey(key)){
singletonMap.put(key,instance);
}
}
}
public static Object getInstance(String key){
return singletonMap.get(key);
}
}
破外单例模式
序列化破坏单例模式
1 | public static void main(String[] args) throws IOException, ClassNotFoundException { |
从结果可见,这是两个不同的对象
解决方式
在单例模式中添加,这边以饿汉式单例为例1
2
3
4//反序列话,在反射中会需要这个方法,定义了之后就会返回同一个对象
private Object readResolve(){
return hungrySingleton;
}
原因
在ObjectInputStream类中,读取时,判断类是否序列化,是的话会反射创建一个新对象1
2
3
4
5
6
7
8
9
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;//创建对象
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
还是在ObjectInputStream类中,会判断是否有readResolve方法,如果有,反射执行该方法,最后返回readResolve方法中的对象。上面的对象还是会创建,但只是不返回而已。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()) //判断是否有readResolve方法
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
反射破坏单例
通过反射来创建单例对象,来判断是否是同一个对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class objectClass = HungrySingleton.class;
Constructor constructor = objectClass.getDeclaredConstructor();
//单例对象
HungrySingleton instance = HungrySingleton.getHungrySingleton();
//设置权限为true
constructor.setAccessible(true);
//反射对象
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
很显然,结果并不是同一个,反射会生成一个新对象。
如何解决
在类加载时就创建对象
饿汉式单例和静态内部类单例都是在类加载时就创建了对象,所以可以在构造函数中进行一个判断来防止反射攻击1
2
3
4
5
6private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException(("禁止反射创建新对象"));
}
}
测试结果
普通懒汉式
如果也是在构造函数中添加判断方法,那么当创建对象时,如果是单例类的获取实例方法先创建,反射方法后调用,那么结果就和上面的一样,会报错,不允许反射对象的创建。
如果反射方法先调用,那么就会出现两个对象。即使我们在单例类中创建一个标记,也是可以通过反射进行修改的。所以并不能完全阻止。