来源:https://blog.csdn.net/ayunlong/article/details/130297205?spm=1001.2014.3001.5502

Java设计模式-单例模式(Singleton Pattern)

单例模式可以非常有效的节省系统开支

什么是单例模式?

单例就是单个实例,在进程所分配的内存中仅能存在唯一一个对象。

单例模式就是通过技术手段,来实现这个对象在内存中是唯一的。

为什么要使用单例模式?
实例的创建需要系统资源的开支,特别是类功能负责、读取IO、网络、数据库连接等操作更是非常大的开支,那么创建一个实例,所有的功能都使用该实例是不不是就可以节省系统开支。

什么是单例模式

单例模式的实现方式及线程安全问题

第一种:饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.singleton;

/**
* 单例模式:饿汉式
*/
public class HungryPrinter {
// 打印机实例
private static final HungryPrinter instance = new HungryPrinter();
// 私有构造函数,禁止使用new创建对象
private HungryPrinter(){}
// 获取打印机对象
public static HungryPrinter getInstance(){
return instance;
}
}

饿汉式的有以下几个特点,这三个特点共同完成了单个实例的限制:

  • 使用private私有化实例;final限制修改对象;static在类加载时初始化Printer对象
  • 私有化构造函数,禁止主动new对象
  • 只能通过getInstance获取实例

static关键字决定了,类加载时便会初始化Printer对象,哪怕没有使用这个对象,这是一种资源浪费。

第二种:懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.singleton;

public class LazyPrinter {
// 打印机实例
private static LazyPrinter instance;
// 私有构造函数,禁止使用new创建对象
private LazyPrinter(){}

public static LazyPrinter getInstance(){
// 只有当对象为空时,才会创建对象
if (instance == null){
instance = new LazyPrinter();
}
return instance;
}
}

上面的代码是不是看来很完美,解决了饿汉式的资源浪费问题,但引入了新的问题,多线程安全!多么头疼的词语。什么情况下会引起多线程安全?

  • 存在多线程同时访问一个资源(同时访问instace)
  • 访问非方法内部的局部变量(instance就是非方法内部的局部变量)

刚写的懒汉式代码完全符合,假设T1与T2两个线程,同时调用了getInstance方法,T1运行到if (instance == null)处后CPU时间片用完了,这时候轮到T2,T2执行的时候T1还没有成功创建,所以成功创建了对象,当T1重新拿回执行权时,恰好也执行了new操作,又重新创建了对象, 这样就破坏了单例模式。

第三种:双锁检测(double check)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.singleton;

public class DoubleCheckPrinter {
// 打印机实例
private static volatile DoubleCheckPrinter instance;
// 私有构造函数,禁止使用new创建对象
private DoubleCheckPrinter(){}

public static DoubleCheckPrinter getInstance(){
if (instance == null){
synchronized (DoubleCheckPrinter.class){
if(instance == null){
instance = new DoubleCheckPrinter();
}
}
}
return instance;
}
}

volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

第四种:静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.singleton;

public class InnerClassPrinter {
// 私有构造函数,禁止使用new创建对象
private InnerClassPrinter(){}
//提供公共的访问方式
public static InnerClassPrinter getInstance(){
return InnerClassHolder.INSTANCE;
}

private static final class InnerClassHolder {
private static final InnerClassPrinter INSTANCE = new InnerClassPrinter();
}
}

感觉和饿汉模式很类似,就是把实例化的过程放到了内部类,解决了类加载时便会初始化实例的缺陷,同时也解决了懒汉式的线程安全问题。

第五种:枚举单例

枚举类实现单例模式,是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,而且枚举类型是所有单例实现中唯一一种不会被破坏的单例模式

1
2
3
4
5
6
/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}

说明:枚举方式属于饿汉式的方法

反序列化破坏单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.singleton;
//先让DoubleCheckPrinter类实现序列化接口
public class Test {
public static void main(String[] args) throws Exception {
// 将DoubleCheckPrinter对象序列化和到printer.txt
DoubleCheckPrinter printer = DoubleCheckPrinter.getInstance();
FileOutputStream fileOutputStream = new FileOutputStream(new File("printer.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream);
oos.writeObject(printer);

// 将printer.txt的对象反序列化到Java中
FileInputStream inputStream = new FileInputStream("printer.txt");
ObjectInputStream oosInput = new ObjectInputStream(inputStream);
System.out.println(printer == oosInput.readObject());

}
}
// 运行结果
false

解决办法:
在类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就会返回这个方法的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.singleton;

public class InnerClassPrinter implements Serializable {
// 私有构造函数,禁止使用new创建对象
private InnerClassPrinter(){}

public static InnerClassPrinter getInstance(){
return InnerClassHolder.INSTANCE;
}

private static final class InnerClassHolder {
private static final InnerClassPrinter INSTANCE = new InnerClassPrinter();
}
//反序列化时会自动调用该方法,将该方法的返回值直接返回
public Object readResolve(){
return InnerClassPrinter.INSTANCE;
}
}

反射破坏单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package org.singleton;

import java.lang.reflect.Constructor;

public class Test {
public static void main(String[] args) throws Exception {
DoubleCheckPrinter printer = DoubleCheckPrinter.getInstance();
DoubleCheckPrinter printer1 = DoubleCheckPrinter.getInstance();

System.out.println("通过getInstance获取的对象是否为同一个对象:" + (printer == printer1));

Class<?> cls = DoubleCheckPrinter.getInstance().getClass();
Constructor constructor = cls.getDeclaredConstructor();
constructor.setAccessible(true);
DoubleCheckPrinter printer2 = (DoubleCheckPrinter) constructor.newInstance();
System.out.println("通过反射获取的对象是否为同一个对象:" + (printer == printer2));
}
}
// 执行结果如下
通过getInstance获取的对象是否为同一个对象:true
通过反射获取的对象是否为同一个对象:false

解决方法

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
package org.singleton;

public class InnerClassPrinter {
private static boolean flag = false;
// 私有构造函数,禁止使用new创建对象
private InnerClassPrinter(){
/*
反射破解单例模式需要添加的代码
*/
//判断flag的值是否是true, 如果是true, 说明非第一次访问,直接抛出异常,如果时false的话,说明第一次访问
synchronized(InnerClassPrinter.class){ //防止出现线程安全问题
if (flag){
throw new RuntimeException("不能2创建多个对象");
}

flag = true ;
}
}
//提供公共的访问方式
public static InnerClassPrinter getInstance(){
return InnerClassHolder.instance;
}

private static volatile InnerClassPrinter instance;

private static final class InnerClassHolder {
if(instance != null){
return instance;
}
private static final InnerClassPrinter instance = new InnerClassPrinter();
}
}

Struts2单例模式的应用