单例设计模式

1、设计模式概述和分类(三种类型共23种)

设计模式是程序员在面对同类软件工程设计问题所总结出来的有用的经验,模式不是代码,而是某类问题的通用解决方案,设计模式代表了最佳的实践。这些解决方案是众多软件开发人员经过相当长的一段时间的实验和错误总结出来的。

设计模式的本质是提高软件的维护性,通用性和扩展性,并降低软件的复杂度。


【1】创建型模式:单例模式、抽象工厂模式、原型模式、建造者模式、工厂模式。

创建型模式主要关注点是”怎样创建对象?”,它的主要特点是”将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

【2】结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
【3】行为型模式:模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)。

2、单例模式

所谓类的单例设计模式,就是采取一定的方法保证在整个软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

单例模式有八种方式:饿汉式(静态常量)、饿汉式(静态代码块)、懒汉式(线程不安全)、懒汉式(线程安全,同步方法)、懒汉式(线程安全,同步代码块)、双重检查、静态内部类、枚举


【1】饿汉式(静态常量)

1)构造器私有化(防止new对象)

2)类的内部创建对象

3)向外暴露一个静态的的公共方法。getInstance()

4)代码实现

1
2
3
4
5
6
7
8
public class Singleton {
// 类加载时就创建对象
private static Singleton single = new Singleton();
private Singleton(){}
public static Singleton getInstance() {
return single;
}
}

5)优缺点说明

① 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

② 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存浪费。

③ 这种方式基于classloader机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类装载的原因有很多,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果。

④ 结论:这种单例模式可用,可能造成内存浪费。


【2】饿汉式(静态代码块)

1)代码实现

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton single;
// 在静态代码块执行时,创建单例对象
static {
single = new Singleton();
}
private Singleton(){}
public static Singleton getInstance() {
return single;
}
}

2)优缺点说明

① 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。

② 结论:这种单例模式可用,但是可能造成内存浪费。


【3】懒汉式(线程不安全)

1)代码实现

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton single;
private Singleton(){}
// 当使用到该方法时才创建instance
public static Singleton getInstance() {
if (null == single) {
single = new Singleton();
}
return single;
}
}

2)优缺点说明

① 起到了Lazy Loading的效果,但是只能在单线程下使用。

② 如果在多线程下,一个线程进入了if判断语句,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

③ 结论:在实际开发中,不要使用这种方式。


【4】懒汉式(线程安全,同步方法)

1)代码实现

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton single;
private Singleton(){}
// 加入同步代码,解决线程不安全问题
public static synchronized Singleton getInstance() {
if (null == single) {
single = new Singleton();
}
return single;
}
}

2)优缺点说明

① 解决了线程不安全问题。

② 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低了。

③ 结论:在实际开发中,不推荐使用这种方式。


【5】懒汉式(线程不安全,同步代码块)

1)代码实现

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton single;
private Singleton(){}
public static Singleton getInstance() {
if (null == single) {
synchronized(Singleton.class) {
single = new Singleton();
}
}
return single;
}
}

2)优缺点说明

① 这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低,改为同步产生实例化的代码块。

② 但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if条件,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

③ 结论:在实际开发中,不能使用这种方式。


【6】双重检查

1)代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private static volatile Singleton single;
private Singleton(){}
public static Singleton getInstance() {
// 加入双重校验代码,解决了线程安全问题,同时解决懒加载问题。
if (null == single) {
synchronized(Singleton.class) {
if (null == single) {
single = new Singleton();
}
}
}
return single;
}
}

2)优缺点说明

① Double-Check概念是多线程开发中经常使用到的,如代码中所示,我们进行了两次if检查,这样就可以保证线程安全了。

该模式解决了单例、性能、线程安全问题,上面的Double-Check在多线程情况下,如果single没有被volatile修饰就会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排操作。所以要使用volatile才能保证可见性和有序性。

② 这样,实例化代码只用执行一次,后面再次访问时,判断if后直接return实例化对象,也避免了反复进行方法同步。

③ 线程安全:延迟加载,效率较高。

④ 结论:在实际开发中,推荐使用这种单例设计模式。


【7】静态内部类

1)代码实现

静态内部类单例模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被static修饰,保证只被实例化一次,并且严格保证实例化顺序。

1
2
3
4
5
6
7
8
9
public class Singleton {
private Singleton(){}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}

2)优缺点说明

① 这种方式采用了类装载机制来保证初始化实例时只有一个线程。

② 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

③ 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

④ 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高。

⑤ 结论:推荐使用。


【8】枚举(属于饿汉式)

1)代码实现

枚举类实现单例模式是非常推荐的,因为枚举类是线程安全的,并且只会装载一次,设计者充分利用了枚举的这个特性来实现单例模式,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

1
2
3
4
5
6
enum Singleton {
INSTANCE; // 属性
public void sayOK() {
System.out.println("ok~");
}
}

2)优缺点说明

① 这借助JDK1.5中添加的枚举来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重写创建新的对象。

② 这种方式是Effective Java作者Josh Bloch提倡的方式。

③ 结论:推荐使用。


3、单例模式注意事项

【1】JDK源码中的java.lang.Runtime类就是单例模式,使用到是饿汉模式创建的。
【2】单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
【3】当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new。
【4】单例模式使用的场景:需要频繁的进行创建和销毁对象、创建对象时耗时过多或耗费资源过多(即重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等)。

4、单例模式存在的问题

【1】破坏单例模式

使所定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别式序列化和反射。

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
46
47
48
49
public class Singleton implements Serializable{
private Singleton(){}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}


// 测试使用序列化破坏单例模式
public class Client {
public static void main(String[] args) {
// writeObject2File();
// readObjectFromFile();
// readObjectFromFile();
// readObjectFromFile();
// 三次打印,内存地址不同,说明不是同一个对象
}

// 向文件中写对象
public static void writeObject2File() throws Exception {
// 1. 获取Singleton对象
Singleton instance = Singleto.getInstance();

// 2. 创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file.txt"));

// 3. 写对象
oos.writeObject(instance);

// 4. 释放资源
oos.close();
}

// 从文件中读对象
public static void readObjectFromFile() throws Exception {
// 1. 创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("file.txt"));
// 2. 读取对象
Singleton instance = (Singleton) ois.readObject();

// 3. 释放资源
ois.close();

System.out.println(instance);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 测试使用反射破坏单例模式
public class Client {
public static void main(String[] args) {
// 1. 获取Singleton的字节码对象
Class clazz = Singleton.class;
// 2. 获取无参构造方法对象
Constructor cons = clazz.getDeclaredConstructor();
// 3. 取消访问检查
cons.setAccessible(true);
// 4. 创建Singleton对象
Singleton s1 = (Singleton) cons.newInstance();
Singleton s2 = (Singleton) cons.newInstance();
System.out.println(s1 == s2); // false
}
}

【2】单例模式被破坏问题的解决

序列化、反序列化方式破坏单例模式的解决方法。

在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton implements Serializable{
private Singleton(){}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
// 当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回
// 可以debug 上述ois.readObject()的函数调用,观察是否真正调用
public Object readResolve() {
return SingletonInstance.INSTANCE;
}
}
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
// 源码解析,ObjectInputStream类
public final Object readObject() throws IOException, ClassNotFoundException {
...
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
// 重点查看readObject方法
Object obj = readObject0(false);
}
}

private Object readObject0(boolean unshared) throws IOException {
...
try {
switch (tc) {
...
case TC_OBJECT:
// 重点查看readOrdinaryObject方法
return checkResolve(readOrdinaryObject(unshared));
...
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}

private Object readOrdinaryObject(boolean unshared) throws IOException {
...
// isInstantiable返回true,执行desc.newInstance(),通过反射创建新的单例类,
obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// 在Singleton类中添加readResolve方法后desc.hasReadResolveMethod()方法执行结果为true
if(obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) {
// 通过反射调用Singleton类中的readResolve方法,将返回值赋值给rep
// 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象
Object rep = desc.invokeReadResolve(obj);
...
}
return obj;
}

反射方式破解单例的解决方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton{
private static volatile Singleton instance;

// 私有构造方法
private Singleton(){
// 反射破解单例模式需要添加的代码
if(instance != null) {
throw new RuntimeException();
}
}

public static Singleton getInstance() {
return instance;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton implements Serializable{
private Singleton(){
if(SingletonInstance.INSTANCE != null) {
throw new RuntimException("不能创建多个对象");
}
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton implements Serializable{
private static boolean flag = false;

private Singleton(){
synchronized(Singleton.class) {
if(flag) {
throw new RuntimException("不能创建多个对象");
}
flag = true;
})
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}

5、JDK源码解析-Runtime类

【1】Runtime类就是使用的单例设计模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Runtime {
private static final Runtime currentRuntime = new Runtime();

private static Version version;

/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class {@code Runtime} are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the {@code Runtime} object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}

...
}
1
2
3
4
5
6
7
8
9
class Client{
public static void main(String[] args) throws Exception{
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("ipconfig");
InputStream inputStream = process.getInputStream();
byte[] bytes = inputStream.readAllBytes();
System.out.println(new String(bytes, "GBK"));
}
}