创建者模式---单例模式

创建者模式的主要关注点是”怎样创建对象”,主要特点是”将对象的创建与使用分离”

这样降低了系统耦合度,使用者不需要关注对象的创建细节。

创建者模式分为:

  • 单例模式
  • 工厂方法模式
  • 抽象工程模式
  • 原型模式
  • 建造者模式

今天先学习一下单例模式

什么是单例设计模式(Singleton Pattern)

单例设计模式是Java中最简单的设计模式之一。属于创建型模式,提供了创建对象的最佳方式。

该模式涉及到一个单一的类,该类负责创建自己的对象,且确保只有一个实例被创建。这个类同时也提供了一种访问其唯一实例的方式。

因此单例模式主要有以下角色:

  • 单例类 只能创建一个实例的类
  • 访问类 使用单例类

单例模式的实现

饿汉式

类加载就会导致该单实例对象被创建

静态变量方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 单例模式-饿汉式-静态变量方式
* 该方式在成员位置声明Singleton类的对象instance。实例对象是随着类的加载而创建的。如果该对象足够大的话,而一直不使用会造成内存的浪费。
*/
public class Singleton {

// 1.私有构造方法
private Singleton() {
}

// 2.在本类中创建本类对象
private static Singleton instance = new Singleton();

// 3.提供外界一个公共的访问方式,让外界获取该对象
public static Singleton getInstance() {
return 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
/**
* 单例模式-饿汉式-静态代码块
* 同方式一,也存在浪费问题
*/
public class Singleton {

// 私有构造方法
private Singleton() {
}

// 声明Singleton类型的变量
private static Singleton instance; // null

// 在静态代码块中进行赋值
static {
instance = new Singleton();
}

public static Singleton getInstance() {
return instance;
}

}

以上两种方式在类的加载时就会创建对象,但如果实例化的对象长时间不被使用,则都会带来一定的内存开销。因此并不推荐饿汉式的声明方式。

懒汉式

类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

方式一(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 单例模式-懒汉式-方式1(线程不安全)
*/
public class Singleton {

// 私有构造方法
private Singleton() {
}

// 声明Singleton类型的变量instance
private static Singleton instance; // 只是声明一个该类型的变量,并没有赋值

// 对外提供访问方式
public static Singleton getInstance() { // 每调用这个方法,都会创建一个对象
if (instance == null) {
instance = new Singleton();
}
return instance;
}

}

方式一当有多个先线程在获取单例方法中等待CPU执行权时,导致有多个对象被实例化

方式二(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 单例模式-懒汉式-方式2(线程安全)
*/
public class Singleton {

// 私有构造方法
private Singleton() {
}

// 声明Singleton类型的变量instance
private static Singleton instance; // 只是声明一个该类型的变量,并没有赋值

// 对外提供访问方式
public static synchronized Singleton getInstance() { // 每调用这个方法,都会创建一个对象
if (instance == null) {
instance = new Singleton();
}
return instance;
}

}

这种方法一改方式一,在getInstance方法上加了同步锁,使得线程安全,但由于直接给方法上锁,使得锁粒度过高,大大降低了程序的性能

方式三(双重检查锁)

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
/**
* 单例模式-懒汉式-双重检查锁
*/
public class Singleton {

// 私有构造方法
private Singleton() {
}

// volatile关键字保证可见性和有序性,解决多线程带来的空指针问题
private static volatile Singleton instance;

public static Singleton getInstance() {
// 第一次判断,如果instance值部位null,则不需要抢占锁
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判断
if (instance == null) {
instance = new Singleton();
}
}

}
return instance;
}
}

在判断为空后,Singleton加上类锁,再进行一次判断,为空则实例化对象。此方法也是一种常见的单例模式的实现方式

方式四(静态内部类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 静态内部类方式
*/
public class Singleton {

private Singleton() {
}

// 定义一个静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}


}

在第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder,并初始化INSTANCE,这样不仅能确保线程安全,也能保证Singleton类的唯一性。静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。它利用了JVM的特性,在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和间的浪费。

枚举方式

单元素的枚举类型已经成为实现Singleton的最佳方法

这句话是Joshua Bloch前辈在《Effective Java》一书中提到的。此方式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

1
2
3
4
5
6
/**
* 枚举方式
*/
public enum Singleton {
INSTANCE
}
1
2
3
4
5
6
7
8
9
10
11
public class Client {

public static void main(String[] args) {

Singleton instance = Singleton.INSTANCE;
Singleton anotherInstance = Singleton.INSTANCE;
System.out.println(instance == anotherInstance);

}
}

1
2
3
true

进程已结束,退出代码0

枚举方式属于饿汉式,在不考虑浪费内存空间的情况下,可首选枚举法。上面提到了,枚举方式可以避免反序列化重新创建新的对象,那么看来单例是可以被破坏的,下面就来分析分析

存在的问题

上面的方式,枚举除外,都可以创建多个对象。有两种实现方式:序列化和反射

序列化

采用懒汉式静态内部类方式创建单例(要继承序列化接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton implements Serializable {

private Singleton() {
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.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
public class Client {

public static void main(String[] args) throws Exception {

writeObject2File();
readObjectFromFile();
readObjectFromFile();

}

// 从文件中写对象
public static void readObjectFromFile() throws Exception {
// 1.创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Lin\\Desktop\\demo.txt"));
// 2.读取对象
Singleton instance = (Singleton) ois.readObject();
System.out.println(instance);
// 3.释放资源
ois.close();

}

// 从文件中读对象
public static void writeObject2File() throws Exception {
// 1.获取对象
Singleton instance = Singleton.getInstance();
// 2.创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Lin\\Desktop\\demo.txt"));
// 3.写对象
oos.writeObject(instance);
// 4.释放资源
oos.close();
}

}
1
2
3
4
tech.maiquer.signleton.destroy.demo1.Singleton@312b1dae
tech.maiquer.signleton.destroy.demo1.Singleton@7530d0a

进程已结束,退出代码0

说明:将单例对象流读取写到桌面的demo文件中,再读取文件取出对象打印出来,发现两次地址并不统一,说明序列化破坏了单例

反序列破解解决方法

只需要在Singleton类中增加readResolve方法

1
2
3
4
5
6
7
8
/**
* 解决反序列化破解单例模式
*
* @return
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}

测试:

1
2
3
4
tech.maiquer.signleton.destroy.demo1.Singleton@3764951d
tech.maiquer.signleton.destroy.demo1.Singleton@3764951d

进程已结束,退出代码0

现在就解决了序列化破坏单例的问题了

反射

先同样使用懒汉式静态内部类的方式构造单例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {

private Singleton() {
}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Client {

public static void main(String[] args) throws Exception {

// 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);

}

}

通过反射,拿到对象构造器,无视private权限

打印结果如下:

1
2
3
false

进程已结束,退出代码

发现反射毫无感情的破坏了单例(我单例不要面子的吗…)或许这就是暴力美学吧~~~

解决反射

魔高一尺,道高一丈

既然反射可以任性的获取构造器构造实例,我们拦不住。那就加一个flag标志,限制该类只能创建一个实例

同样以懒汉式静态内部类为例:

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
public class Singleton {

// 初始化为false
private static boolean flag = false;

private Singleton() {

synchronized (Singleton.class) { // 方法同步锁,使线程安全

if (flag) { // 如果flag为真,则说明已经创建过实例了
throw new RuntimeException("唯一实例已被创建,不能再创建啦!!!");
}
// 实例被创建了 则将flag设为真、
flag = true;

}

}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

}

客户端测试:

1
2
3
4
5
6
7
8
9
10
11
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at tech.maiquer.signleton.destroy.demo2.Client.main(Client.java:17)
Caused by: java.lang.RuntimeException: 唯一实例已被创建,不能再创建啦!!!
at tech.maiquer.signleton.destroy.demo2.Singleton.<init>(Singleton.java:13)
... 5 more

进程已结束,退出代码1

可见,程序出发了运行时异常,告诉我们唯一实例已存在,休想再贪

JDK源码实例

JDK中的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 Runtime currentRuntime = new Runtime();

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

/** Don't let anyone else instantiate this class */
private Runtime() {}

}

通过前面的铺垫,不难看出,这是一个通过饿汉式创建单例的案例

该类有很多好玩的函数,例如exec方法,它可以运行终端命令,例如我要获取我的电脑的网卡信息:

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
import java.io.IOException;
import java.io.InputStream;

public class RuntimeDemo {

public static void main(String[] args) throws IOException {

// 获取Runtime单例
Runtime runtime = Runtime.getRuntime();

// 调用runtime的exec方法
String command = "ipconfig";
Process process = runtime.exec(command);

// process对象获取输入流
InputStream is = process.getInputStream();
byte[] arr = new byte[1024 * 1024 * 100];

// 读取数据
int len = is.read(arr); // 返回读到的字节的个数

// 打印
System.out.println(new String(arr, 0, len, "GBK"));

}

}

打印结果:

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
50
51
52
53
54
Windows IP 配置


以太网适配器 以太网:

媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

无线局域网适配器 本地连接* 10:

媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

无线局域网适配器 本地连接* 11:

媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

以太网适配器 SSTAP 1:

媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

以太网适配器 VMware Network Adapter VMnet1:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::70c5:6a39:2ffc:e4d1%16
IPv4 地址 . . . . . . . . . . . . : 192.168.35.1
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :

以太网适配器 VMware Network Adapter VMnet8:

连接特定的 DNS 后缀 . . . . . . . :
本地链接 IPv6 地址. . . . . . . . : fe80::9506:5df3:8e59:c204%9
IPv4 地址 . . . . . . . . . . . . : 192.168.221.1
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :

以太网适配器 蓝牙网络连接:

媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :

无线局域网适配器 WLAN:

连接特定的 DNS 后缀 . . . . . . . :
IPv4 地址 . . . . . . . . . . . . : 192.168.3.119
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.3.1


进程已结束,退出代码0

结束语:

我是自己最大的敌人 — 拿破仑