单例模式

什么是单例模式

保证一个类仅有一个实例,并提供一个全局访问点。

单例优点

  • 在内存中只有一个实例,减少内存开销;
  • 可以避免对资源的多重占用;
  • 设置全局访问点,严格控制访问(外部不能被new出来,只能使用提供的方法);

单例缺点

  • 没有接口,扩展困难

    单例重点

  1. 构造器是私有的
  2. 线程安全
  3. 延迟加载(在使用时才加载)
  4. 序列化和反序列化安全
  5. 反射

单例模式的几种写法

懒汉模式

注重延迟加载,在使用时才进行加载

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LazyDoubleCheckSingleton {
//使用volatile保证内存的可见性,不允许创建对象时的重排序
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;

private LazyDoubleCheckSingleton() {
}

public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton == null){
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
//new 对象分为3个操作
//1.分配内存给这个对象
//2.初始化对象(2和3会重排序)
//3.设置lazyDoubleCheckSingleton指向分配的内存地址
}
}
}
return lazyDoubleCheckSingleton;
}
}

静态内部类的方式

通过Class对象的初始化锁,保证在对象创建过程中,其他线程不会造成影响,只能等待Class对象的锁(静态内部类的初始化锁)

1
2
3
4
5
6
7
8
9
10
11
12
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
}

private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}

public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
}

饿汉式

1
2
3
4
5
6
7
8
9
10
public class HungrySingleton {
private final static HungrySingleton hungrySingleton = new HungrySingleton();

private HungrySingleton(){

}
public static HungrySingleton getHungrySingleton(){
return hungrySingleton;
}
}

优点:写法简单,类加载时就进行了初始化,避免了线程同步问题。
缺点:浪费内存。

枚举单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum EnumInstance {
INSTANCE;
private Object data;

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}

public static EnumInstance getInstance(){
return INSTANCE;
}

}

枚举类序列化 反序列化都是同一个对象,不受序列化的破坏;另外也不能反射攻击

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
45
public 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
15
public 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws IOException, ClassNotFoundException {

HungrySingleton instance = HungrySingleton.getHungrySingleton();
//输出
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
//读取
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));

HungrySingleton newInstance = (HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(newInstance == instance);
}

从结果可见,这是两个不同的对象
序列化反序列话破坏单例.jpg

解决方式

在单例模式中添加,这边以饿汉式单例为例

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
22
if (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
16
public 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);
}

很显然,结果并不是同一个,反射会生成一个新对象。
反射破坏单例.jpg

如何解决

在类加载时就创建对象

饿汉式单例和静态内部类单例都是在类加载时就创建了对象,所以可以在构造函数中进行一个判断来防止反射攻击

1
2
3
4
5
6
private HungrySingleton(){
if(hungrySingleton != null){
throw new RuntimeException(("禁止反射创建新对象"));
}

}

测试结果
禁止反射创建对象.jpg

普通懒汉式

如果也是在构造函数中添加判断方法,那么当创建对象时,如果是单例类的获取实例方法先创建,反射方法后调用,那么结果就和上面的一样,会报错,不允许反射对象的创建。
如果反射方法先调用,那么就会出现两个对象。即使我们在单例类中创建一个标记,也是可以通过反射进行修改的。所以并不能完全阻止。

------------- 感谢阅读-------------