【设计模式】一:单例模式

一、场景问题

    考虑这样一个问题,在我们的项目中需要连接Mysql数据库,数据库连接相关配置信息都写在配置文件中,常用的配置文件有xml和properties格式,那么我们读取配置文件的时候应该怎么做呢?这里我们以properties格式的配置文件为例,在没有使用设计模式的前提下,我们通过Java中读取配置文件的方法将连接信息读取出来放在对象中,然后使用这个对象。

先写一个读取配置文件的类AppConfig:

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
package com.chenxyt.java.test;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class AppConfig {
//定义两个用来存储配置文件内容的字符串
private String parameterA;
private String parameterB;
//访问对象的私有数据域
public String getParameterA() {
return parameterA;
}
public String getParameterB() {
return parameterB;
}
//构造方法
public AppConfig(){
readConfig();
}
//读取配置文件并赋值给存储字符串
private void readConfig(){
//获取一个properties对象的引用
Properties p = new Properties();
//输入流
InputStream in = null;
try{
//输入流获取配置文件
in = AppConfig.class.getResourceAsStream("appConfig.properties");
//输入流加载到properties对象
p.load(in);
//将配置文件的内容赋值到成员变量
this.parameterA=p.getProperty("url");
this.parameterB=p.getProperty("port");
}catch(IOException e){
//读取配置文件异常
e.printStackTrace();
}finally{
try {
//发生异常之后也要关闭输入流所以写在finally块中
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

在AppConfig.java的目录下编写配置文件appConfig.properties

1
2
url=127.0.0.1
port=3306

编写一个用于测试读取配置文件的客户端类Client.java

1
2
3
4
5
6
7
8
9
package com.chenxyt.java.practice;
import com.chenxyt.java.test.AppConfig;
public class Client{
public static void main(String[] args) {
AppConfig ac1 = new AppConfig();
System.out.println("paramA:" + ac1.getParameterA());
System.out.println("paramB:" + ac1.getParameterB());
}
}

运行结果:

png1

    如上所示,利用基本的对象操作,完成了对配置文件的读取。接下来思考一个问题,如果项目中有很多地方需要获取配置文件,那是不是我们需要在多个地方都new出这个对象呢?如果配置文件资源内容过多,那么频繁大量的创建相同的对象,那么将是一个不小的开销。如下:

1
2
3
4
5
6
7
8
9
10
11
12
package com.chenxyt.java.practice;
import com.chenxyt.java.test.AppConfig;
public class Client{
public static void main(String[] args) {
AppConfig ac1 = new AppConfig();
AppConfig ac2 = new AppConfig();
System.out.println("paramA:" + ac1.getParameterA());
System.out.println("paramB:" + ac1.getParameterB());
System.out.println("paramA:" + ac2.getParameterA());
System.out.println("paramB:" + ac2.getParameterB());
}
}

运行结果:

png2

    我们每创建一个对象对象内部的私有数据源就要被使用,并且也占用了大量的内存。事实上对于AppConfig这种公用的类,在运行时获取一次资源就可以了,因为资源都是固定的。

所以上面的问题抽象出来就是,一个类在程序的运行过程中,只需要一个实例,应该怎样做。

二、解决方案

    上述问题的解决方案就是使用单例模式,单例模式的目的是只创造一个类的实例,但是可以多次访问,只不过每次访问的都是之前的实例。这一点有点类似static作用域。其次,对于类中的构造函数,理论上我们可以通过构造函数进行实例的创建,因此我们需要避免用户客户端可以通过构造函数创建实例的方式,也就是使用private作用域修饰构造方法。然后提供一个公用的方法作为接入点,获取单例。因此单例模式的主要实现思路就是使用static+private+新的方法完成。

Java中设计模式主要有两种,懒汉式和饿汉式

懒汉式单例模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.chenxyt.java.test;
public class Singleton{
 //定义一个存放单例对象的变量
 private static Singleton uniqueInstance = null;
 //私有化构造函数,保证实例个数
 private Singleton(){
  //---处理业务,给对象的私有域赋值
 }
 //加锁保证线程安全 提供公共的获取实例方法 设置成静态方法,保证不用对象就可以调用
 public synchronized static Singleton getInstance(){
  //懒汉式设计,如果实例不存在则初始化
  if(uniqueInstance==null){
   uniqueInstance = new Singleton();
  }
  return uniqueInstance;
 }
}

饿汉式单例:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.test;
public class Singleton{
 //定义一个存放实例的变量
 private static Singleton uniqueInstance = new Singleton();
 //私有化构造函数,保证实例个数
 private Singleton(){
  //---处理业务,给对象的私有域赋值
 }
 //提供公共的调用方法 由于只返回实例,所以不存在线程不安全问题
 public static Singleton getInstance(){
  return uniqueInstance;
 }
}

    所谓的懒汉式,就是唯一的类实例只有当马上要使用的时候才会创建,而饿汉式则是比较着急的那种,在类加载的时候就已经创建了该类实例。

现在知道了单例设计模式,那么我们使用单例设计模式重写上边的示例代码。

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
55
56
57
package com.chenxyt.java.test;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class Singleton {
//先创建一个实例
private static Singleton uniqueInstance = new Singleton();

public static Singleton getInstance(){
//返回唯一的类实例
return uniqueInstance;
}
//私有化构造方法,收回创建实例的权限
private Singleton(){
readConfig();
}
//建立两个成员变量存储配置文件的内容
private String parameterA;
private String parameterB;
//获取私有成员变量的方法
public String getParameterA() {
return parameterA;
}
public String getParameterB() {
return parameterB;
}
//读取配置文件并赋值给存储字符串
private void readConfig(){
//获取一个properties对象的引用
Properties p = new Properties();
//输入流
InputStream in = null;
try{
//输入流获取配置文件
in = Singleton.class.getResourceAsStream("appConfig.properties");
//输入流加载到properties对象
p.load(in);
//将配置文件的内容赋值到成员变量
this.parameterA=p.getProperty("url");
this.parameterB=p.getProperty("port");
}catch(IOException e){
//读取配置文件异常
e.printStackTrace();
}finally{
try {
//发生异常之后也要关闭输入流所以写在finally块中
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}

客户端的调用方式也要相应改变一下:

1
2
3
4
5
6
7
8
9
package com.chenxyt.java.practice;
import com.chenxyt.java.test.Singleton;
public class Client{
public static void main(String[] args) {
Singleton sl = Singleton.getInstance();
System.out.println("paraA:" + sl.getParameterA());
System.out.println("paraB:" + sl.getParameterB());
}
}

运行结果如下:

png3

    那么怎样验证单例模式呢?在《Java编程思想》中我们了解到了“==”运算符在比较两个对象的时候,实际上是比较两个对象的引用是否相同,也就是说当两个对象的引用相同的时候,这两个引用实际上是一个,指向同一个内存区域,也就是我们说的只有一个实例。那么我们测试下,在客户端代码中进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.chenxyt.java.practice;
import com.chenxyt.java.test.Singleton;
public class Client{
public static void main(String[] args) {
Singleton sl1 = Singleton.getInstance();
Singleton sl2 = Singleton.getInstance();
if(sl1==sl2){
System.out.println("单例模式成功!只产生了一个实例!");
}else{
System.out.println("单例模式失败!我们是两个不同的实例!");
}
}
}

运行结果:

png4

三、模式讲解

1.单例模式的功能

    单例模式是用来保证程序运行过程中只会产生这一个实例,并且提供一个可以供全局访问的点也就是getInstance()方法来获取这个实例。单例模式只关系实例的创建方式,不涉及具体的业务场景。

2.单例模式的作用范围

    由于单例模式的原理是控制类实例的创建,因此它的作用范围在一个虚机上,因为类的加载过程是在虚机上执行的。所以我们讨论的单例模式只针对单一的系统,不讨论在集群上的情况。同时,通过Java的反射机制也可以创建类的实例,这种情况我们也不考虑,姑且暂认为没有反射机制。

3.懒汉式的实现

    前边我们写了懒汉式的实现示例代码,下面我们分析一下这段代码的设计思路。

a.私有化构造方法:单例模式的核心是收回创建实例的权限,改由自己控制,因此首先一部就是设置构造函数为私有。

1
2
private Singleton(){
}

b.提供获取实例的方法:既然我们回收了创建实例的权限,那么就需要提供一个新的方法用来获取实例。

1
2
public Singleton getInstance(){
}

c.把获取实例的方法变成静态:上边提供了一个获取实例的方法,但是这个方法是实例方法,需要使用实例进行调用,而这个方法恰好是没有实例而创建实例的方法,因此需要将该方法设置成static域,使其可以通过类名.方法的形式进行调用。

1
2
public static Singleton getInstance(){
}

d.定义存储实例的属性:我们需要定义一个变量,用来存储获取的实例,并且将这个变量设置成static以便获取实例的方法可以对其进行访问。

1
private static Singleton instance = null;

e.实现控制实例的创建:我们在getInstance()方法中实现对实例的创建控制,如果存在则返回,不存在则重新创建一个然后返回。

1
2
3
4
5
6
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}

4.饿汉式的实现

    饿汉式与懒汉式的区别在于,饿汉式在程序开始定义变量的时候就已经初始化了,然后在getInstance()方法中直接进行了返回。这里有一个很明显的区别在于

懒汉式:

1
private static Singleton instance = null;

饿汉式:

1
private static Singleton instance = new Singleton();

区别在于:饿汉式的存储变量用到了static的特性!其实static基本就符合了单例设计模式的思想,因为:

1.因为static变量在类加载的时候进行初始化,也就是只初始化一次!

2.多个实例的static变量会共享同一块内存区域,实际上还是只用了一个!

这不正是static要实现的功能吗?!

四、单例模式的延迟加载

    单例模式的懒汉式单例提现了延迟加载的设计思想。那么什么是延迟加载呢?通俗来说,就像懒汉式设计模式那样,在程序启动的时候不去加载资源或者数据,只有等到必须要用不用不行了的时候,才去加载资源或者数据。所以称作是“延迟加载”!这种方法在实际开发中应用较为广泛,因为它尽可能的节约了资源。懒汉式的延迟加载体现如下:

1
2
3
if(instance==null){
instance=new Singleton();
}

现在要使用instance实例了,看一下有没有,没有的话没办法了,只能创建了。

五、单例模式的缓存思想

    缓存思想也是程序设计中的一个常见的功能,简单的说就是某些使用频率较高,系统资源消耗过大的时候,我们可以将这些系统资源放在外部,比如硬盘、数据库中,这样当下次使用的时候就可以先从硬盘或者数据库中获取,如果没有再去内存中获取。这样大大的降低了系统的开销。这样说来跟延迟思想多少有点相似。是的,上述代码中,null实际上就起了一个简单的缓存作用,先判断null是否是对象,如果不是,则创建一个,然后赋值给null,这样下次null就是对象了。

缓存思想是一个典型的使用空间换时间的概念。我们使用Map作为简单的缓存来重新写一下懒汉式的单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.chenxyt.java.test;
import java.util.HashMap;
import java.util.Map;
public class Singleton{
//定义键值对的key
private final static String DEFAULT_KEY = "SingletonKey";
//定义用来缓存的map
private static Map<String,Singleton> map = new HashMap<String,Singleton>();
//私有化构造函数,保证实例个数
private Singleton(){
//---处理业务,给对象的私有域赋值
}
//提供公共的调用方法 由于只返回实例,所以不存在线程不安全问题
public Singleton getInstance(){
Singleton instance = map.get(DEFAULT_KEY);
//缓存中没有就新创建一个然后放到map中
if(instance == null){
instance = new Singleton();
map.put(DEFAULT_KEY, new Singleton());
}
return instance;
}
}

    上述代码中实际上就是用Map代替了原来的null,判断map对应的key是否有值,如果有则返回,没有就创建一个新的然后加到map中去。

单例模式有很多种写法,不管哪种写法其核心思想都是不变的,保证只有一个实例。

六、单例模式的优缺点

1.时间和空间

    比较少上面的代码,懒汉式是典型的时间换空间的设计,每次使用的时候都会判断是否有实例创建。当然如果一直没有人使用,那么会节约内存空间。

    饿汉式是典型的空间换时间,当类装载的时候就会创建实例,每次访问的时候无需判断直接返回实例节约了时间,但是如果一直没有人使用那么会占用系统空间。

2.线程安全

    这里简单说一下线程安全的概念,线程安全是指两个线程同时访问同一个代码区所产生的结果是否安全。显然,不加同步关键字synchronized的懒汉式是线程不安全的,因为两个线程同时访问getInstance方法时,可能会创建两个实例出来。具体来说就是,现在实例instance为null,A线程进入创建实例,创建过程还没有完成也就是还没有将null替代,这时B线程进入,发现instance还是null,于是B线程进入创建实例,等到程序执行完,AB线程都创建了实例。

    饿汉式是线程安全的,因为虚拟机会保证类只被加载一次,而在加载的过程是不会发生并发的。

    解决懒汉式的线程不安全问题可以在方法前边加上synchronized关键字以保证同一时间只有一个线程执行这个方法,此外还有两种方式

a.双重检查锁定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.chenxyt.java.test;
public class Singleton{
//定义一个用来存储变量的值
private static volatile Singleton instance = null;
//私有化构造函数,保证实例个数
private Singleton(){
//---处理业务,给对象的私有域赋值
}
//提供公共的调用方法 由于只返回实例,所以不存在线程不安全问题
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) {
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}

    这里之所有再嵌套一个if判断,是因为假设高并发的情况A跟B都进入第一个if了,那么如果不判断最终还是会有可能创建两个实例。同时这里与其它示例的区别,instance被使用volatile修饰了。这是因为不被volatile修饰同样也会存在高并发下创建两个实例的情况。具体原因是可能会出现第一个线程创建完instance实例之后还没有来得及被第二个进入到if嵌套内部的线程发现,以至于第二个线程认为instance还没有被实例化,所以创建了重复的实例。使用volatile可以保证多线程情况下的资源可见性。具体分析链接:https://www.cnblogs.com/damonhuang/p/5431866.html

b.静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.chenxyt.java.practice;
public class Singleton {
 //定义静态内部类 只有在使用时才被加载
 private static class SingletonHolder{
  //由JVM控制线程安全
  private static Singleton instance = new Singleton();
 }
 //私有化构造方法
 private Singleton(){
  //---
 }
 //提供对外的获取实例的方法
 public Singleton getInstance(){
  return SingletonHolder.instance;
 }
}

    当第一次调用getInstance方法时,它第一次读取LazyHolder.INSTANCE,导致内部类LazyHolder得到初始化,而这个类被装载初始化的时候会初始化其静态域,因此创建了Singleton实例,由于是static的,所以只在类加载的时候实例化了一次。这里简单了解一下上述静态内部类方法的相关基础知识:

1.什么是类级内部类?

    静态内部类也称作类级内部类,顾名思义这个内部类是有static修饰的,如果没有static修饰的内部类则称作是对象级内部类。

2.类级内部类的地位

    类级内部类相当于外部类的static部分,地位与static域或者static方法相同,是一个独立的成员,它的对象与外部类的对象不存在任何依赖关系,因此可以直接创建,而对象级的内部类是绑定在外部对象的实例中。

3.类级内部类中可以定义静态方法,在静态方法中只能够引用外部类中的静态成员方法或者成员变量。

4.如第二条所说,类级内部类相当于其外部类的成员,只有在第一次使用到时才会加载。

    在了解下关于多线程中缺省同步的情况,正常情况下我们一般使用synchronized关键字加锁控制并发,但是有几种情况由JVM自己控制并发。

1.使用static修饰的域、方法、块在加载的时候;
2.访问final字段时;
3.创建线程之前创建对象时;
4.线程可以看见它要处理的对象时。

七、单例和枚举

    JavaSE5之后提供了一种新的数据类型-枚举。单元素的枚举已经成为了实现单例模式的最佳方法。是因为枚举本身也是一个功能齐全的类,它有自己的域和方法,因此是作为单例的基础。其次,enum是通过继承Enum类实现的,所以不能再继承其它的类,但是可以用来实现接口。此外enum类也不能被继承,因为反编译可以发现该类实际上是final类。enum没有public构造器,只有private构造器,这刚好符合了单例模式的思想。

枚举实现单例模式的基本语句如下:

1
2
3
4
5
6
7
package com.chenxyt.java.practice;
enum Singleton{
uniqueInstance;
public void doSomething(){
//===
}
}

模拟使用枚举单例模式创建数据库连接:

1
2
3
4
5
6
7
8
9
10
11
package com.chenxyt.java.practice;
public enum DataSourceEnum {
DATASURCE;
private DBConnection connection = null;
private DataSourceEnum(){
connection = new DBConnection();
}
public DBConnection getConnection(){
return connection;
}
}

数据库链接类:

1
2
3
4
5
package com.chenxyt.java.practice;

public class DBConnection {

}

测试类:

1
2
3
4
5
6
7
8
9
package com.chenxyt.java.practice;

public class Main {
public static void main(String[] args) {
DBConnection conn1 = DataSourceEnum.DATASURCE.getConnection();
DBConnection conn2 = DataSourceEnum.DATASURCE.getConnection();
System.out.println(conn1 == conn2);
}
}

运行结果:

png5

八、总结

    单例模式是较为常用的一种设计模式,掌握单例模式的应用场景以及掌握懒汉式、饿汉式的写法与区别,还有更高级别的使用内部类或者枚举形式的实现。同时也了解懒加载和缓存的设计思想。