设计模式--单例模式

单例模式根据名字就能明白其含义了,就是只能实例化一个对象的意思;多余的话不赘述,其实单例模式在一些场景中使用可以有效的提升服务器效率节省资源利用;比如服务器资源配置文件的读取、数据库连接建立等等,像这种操作没有必要每次随着方法的调用都加载读取一次资源文件,或者重新建立一次新的连接,这样是非常耗时耗资源的操作,我们只需要在服务启动时,或者第一次方法调用时加载一次就行了,后续方法的调用直接使用对应的实例对象即可;实现一个单例模式有很多种方法,每一种方法都尤其应用场景及优缺点,所以下面就分别描述各种方法:

一、非单例模式场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 非单例模式场景
* @author mapingsheng
*/
public class UnSingleton {
private UnSingleton(){
System.out.println("私有构造器");
}
public static UnSingleton getInstance(){
return new UnSingleton();
}
public static void main(String[] args){
UnSingleton myClass = UnSingleton.getInstance();
UnSingleton myClass2 = UnSingleton.getInstance();
System.out.println(myClass == myClass2);
}
}

以上代码运行结果:

private construct

private construct

false

可以看到上面的代码中,每调用一次UnSingleton.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
27
28
29
30
31
32
33
34
35
/**
* 单例模式场景
* @author mapingsheng
*/
public class Singleton {
/**
* 初始化之前 ,先声明一个静态的类变量
*/
private static Singleton unInstance;
private Singleton(){
System.out.println("private construct");
}
/**
* 在静态方法中先判断类变量是否为空,
* 如果为空就实例化一个新的Singleton对象并赋值给类变量
* 然后把实例化的类变量返回
* @return
*/
public static Singleton getInstance(){
if(null == unInstance){
unInstance = new Singleton();
}
return unInstance;
}
public static void main(String[] args){
Singleton sing1 = Singleton.getInstance();
Singleton sing2 = Singleton.getInstance();
System.out.println(sing1 == sing2);
}
}

以上代码运行结果如下:

private construct

true

可以看到,虽然我们调用了Singleton.getInstance()两次,但是只输出了一次private construct,这说明构造方法仅仅执行了一次;主要因为我们定义了静态的类变量Singleton unInstance;当第一次调用Singleton.getInstance()方法时,类变量unInstance为空,所以就new了一个新的Singleton对象,并赋值给类变量unInstance;当第二次调用Singleton.getInstance()时,此时的类变量unInstance不为空,所以直接返回了类变量实例,也就达到了虽然多次调用Singleton.getInstance(),但只实例化一个Singleton对象的目的;并且最后我们通过sing1 == sing2结果为true发现两个分别调用两次Singleton.getInstance()方法产生的两个变量其实是一个对象。

但是上面的例子有一个问题,那就是多线程问题,暂且不说java的指令重排序,仅仅举例说明一下多线程执行会出现实例化多个对象的问题。看getInstance方法中的一段代码

1
2
3
4
5
6
7
public static Singleton getInstance(){
if(null == unInstance){ //第1步
unInstance = new Singleton(); //第2步
} //第3步
return unInstance; //第4步
}

比如我们有两个线程A、线程B同时在运行,线程A在CPU1上面运行,线程B在CPU2上面运行

线程A执行到第2步

线程B执行到第1步

那么虽然线程A执行到了第2步,但是同一时刻线程B已经执行到了第1步;由于线程A在第2步还未执行完,所以线程B在第1步时的判断条件就为true,所以线程B将再次执行第2步,虽然在线程B执行到第2步时,线程A已经执行完了第2步并且已经实例化了Singleton对象,但是线程B仍然会再实例化一次Singleton对象。故就会出现多线程环境下,会实例化多个对象的问题;可以看看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args){
for(int i=0;i<100;i++){
Runnable runn1 = new Runnable() {
@Override
public void run() {
Singleton sing1 = Singleton.getInstance();
}
};
Thread thread1 = new Thread(runn1);
thread1.start();
}
}

运行以上main方法结果:

private construct

private construct

以上输出两次的结果不是绝对的,有可能也会出现输出一次的情况,因为是多线程的嘛(可能两个线程的先后执行的时间稍微长一些,第一个线程执行完getInstance方法后,第二个线程才进入getInstance方法,故第二个线程执行到if判断时就为false,所以就直接返回了线程1已经实例化过的类变量对象);所以为了防止多线程问题,我们下面编写一个线程安全的单例模式代码。

三、单例模式场景-初级(线程安全)

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
/**
* 单例模式场景-线程安全
* @author mapingsheng
*/
public class Singleton {
/**
* 初始化之前 ,先声明一个静态的类变量
*/
private static Singleton unInstance;
private Singleton(){
System.out.println("private construct");
}
/**
* 在静态方法中先判断类变量是否为空,
* 如果为空就实例化一个新的Singleton对象并赋值给类变量
* 然后把实例化的类变量返回
* @return
*/
public static Singleton getInstance(){
//在方法中把实例化操作进行了同步处理操作
synchronized(Singleton.class){
if(null == unInstance){
unInstance = new Singleton();
}
return unInstance;
}
}
public static void main(String[] args){
//多线程调用getInstance方法
for(int i=0;i<100;i++){
Runnable runn1 = new Runnable() {
@Override
public void run() {
Singleton sing1 = Singleton.getInstance();
}
};
Thread thread1 = new Thread(runn1);
thread1.start();
}
}
}

以上代码输出如下:

private construct

不管我们执行多少次,都是只输出一次,所以通过在实例化方法中加入同步处理后,多线程环境下面调用可以防止产生多个实例的问题,但是这种方式不太高效,比如一个访问量很高的网站,那么多线程在高并发运行,每一次请求产生的每一个线程每一次进入到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
27
28
29
30
31
32
33
/**
* 单例模式-中级-线程安全-双重检查锁
* @author mapingsheng
*/
public class Singleton3 {
private volatile static Singleton3 instance;
private Singleton3(){
System.out.println("private construct");
}
private static Singleton3 getInstance(){
if(null == instance){ //进入同步代码块之前先判断instance是否已经实例化,如果已经实例化了就直接返回
synchronized (Singleton3.class) {
if(null == instance){
instance = new Singleton3();
}
}
}
return instance;
}
public static void main(String[] args) {
for(int i=0;i<1000;i++){
new Thread(""+i){
public void run(){
Singleton3 single1 = Singleton3.getInstance();
}
}.start();
}
}
}

以上代码运行结果如下:

private construct

以上代码在多线程环境下面一直会只输出一次,也就是只会实例化一次;因为我们使用了以下方案去处理多线程安全问题

1、使用双重检查-分别在同步代码块外、同步代码块内进行if判断

在使用双重检查后,我们可以在多线程高并发请求环境下提高执行效率,避免过多线程等待操作

也就是最开始的一个线程一旦实例化对象之后,后续的线程进入getInstance方法中后,先执行最外层的if判断,如果之前线程已经实例化完毕,则类变量instance就不为空,所以也就不会再进入同步代码块中,可以有效降低后续无用的进入同步代码块的次数

在同步代码块内又添加了一个if判断操作,是因为在多线程环境下程序几乎是并行处理的,也就是说线程A、线程B都执行到最外层if判断后,然后都会返回true,然后其中一个线程A进入同步代码块中,线程B处于等待状态,等线程A执行完实例化之后,线程B会再次进入同步代码块中,所以此时我们在同步代码块中添加的if判断就起到了避免重复实例化多个对象的作用。

2、使用volatile声明类变量

我们在类变量声明中指定了volatile关键字,之所以添加这个声明,是为了防止虽然我们进行了双重检查,但是java执行指令重排序后,仍然会发生不可预知的问题:

在上面的例子中使用了双重检查后,看似非常完美了,但是有时候仍会出现未知的错误-有可能偶尔还会出现虽然if判断类变量instance已经不为空了,但是其实被实例化的对象中的属性并没有被初始化完毕,主要是因为java虚拟机进行了指令重排序的结果:

>

当线程进入同步代码块中,执行instance = new Singleton3();这一行代码时,这一句实例化代码时,其实这个实例化操作有以下几个步骤:

线程会首先在内存中创建一个副本

调用构造方法,初始化类中的属性

在内存中为该类分配地址,并把地址存入线程自己的内存副本空间中

然后线程会把自己副本中的已实例化的对象的地址赋值给类变量instance,并更新主内存

但是通过java指令重排序以后,可能上面两步骤的顺序是相反的;也就是分配内存地址,然后把内存地址赋值给类变量,那么我们通过if判断时,由于该类在内存中已经存在,所以会错误的认为该类已经实例化完毕了,其实呢该类中的一些初始化操作还没有执行完毕(比如从配置文件中读取相关数据、类属性的赋值操作等等);这就导致我们拿到返回的instance实例后,获取使用其中的属性仍然为空的问题;

所以针对上面的问题,我们就在类遍历中添加了volatile声明,所以就可以避免上述指令出排序的问题

四、单例模式场景-中级(线程安全-静态实例)

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
/**
* 线程安全的
* @author mapingsheng
*/
public class Singleton2 {
//直接把实例化操作作为静态的类变量,也就是类变量会
private static Singleton2 instance = new Singleton2();
private Singleton2(){
System.out.println("private construct");
}
public static Singleton2 getInstance(){
return instance;
}
public static void main(String[] args){
for(int i=0;i<100;i++){
Runnable runn1 = new Runnable() {
@Override
public void run() {
Singleton2 sing1 = Singleton2.getInstance();
}
};
Thread thread1 = new Thread(runn1);
thread1.start();
}
}
}

以上代码运行结果:

private construct

上面的代码多线程环境下面也会仅仅输出一次,当类装载的时候就会创建类的实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断,节省了运行时间;因为不是在运行时执行的实例化操作,所以是线程安全的。