Java常用的设计模式:单例模式

    我大概在脑中规划了一下自己的技术成长路线,包括从基本的编程语言到数据库、网络编程这些,其中设计模式是一块,因为我本人有大概两年的工作经验,所以多少了解一些常用的设计模式,这里借着《研磨设计模式》这本书系统的学习一下,也避免长期学习《Java编程思想》显得枯燥乏味。《Java编程思想》只是本人对基础知识的一个回归。同时也由于时间有限,短期只针对常用的设计模式进行学习研究。个人觉得《研磨设计模式》这本书写的还是比较详细简单易懂,有读者需要的话可以留邮箱我发给你。

    希望自己在技术成长的道路上能够坚持下去。最近项目不是太忙,所以抓紧时间学习成长一下,也是针对项目中用到的具体技术做一个深度的了解。

一、场景问题

    考虑这样一个问题,在一个项目中要连接MySQL数据库,连接的地址以及口令都写在了配置文件中,一般的有xml格式或者是properties格式,那么我们要读取配置文件应该怎样读呢?

    通常情况下,在不用设计模式的前提,我们可以使用Java中的读取配置文件的方法,然后把内容读取出来之后放在对象中。为了操作简单,这里采用读取propertie格式的配置文件。
先写一个读取配置文件的类AppConfig:
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

url=127.0.0.1
port=3306

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

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());
	}	
}

运行结果:

     Java常用的设计模式:单例模式_第1张图片
如上所示,利用基本的对象操作,我们成功的读取了配置文件中的内容。
    思考上边的程序,客户端通过new一个AppConfig的对象实例来完成获取配置文件的内容,看上去似乎没有什么毛病。但是设想一下,假如程序中有很多需要获取配置文件的地方,那么这些地方就都要new一个对象出来,假如配置文件的内容庞大,那么这将是一个不小的资源消耗。比如我们将刚才的Client修改一下,再创建一个对象。
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());
	}	
}

运行结果如下:

     Java常用的设计模式:单例模式_第2张图片

如图所示,我们每创建一个对象,对象内部的私有数据域就会被使用,这浪费了很多的资源。同时创建实例化对象也会占用大量的系统资源。事实上对于AppConfig这种类,在运行的时候获取一次资源就可以了。因为配置文件这些内容都是固定的。

    把上面的问题抽象一下,问题就出来,在一个程序运行的过程中,某个类只需要一个实例,那么该怎么样做呢?

二、解决方案

    上边的问题描述大概意思就是怎么样只创建一个类的实例,也就是单例,这里就用到了单例设计模式。设计模式是在大神们长期开发各种各样的程序中总结出来的一类模板,也就是一种开发模式。单例模式就是保证类只有一个实例,并且提供一个可以供全局访问的点。毕竟我们设计单例模式的初衷不是说只能访问一次,而是只有一个实例可以供多个客户端访问。
    回顾上边的问题,在结合《Java编程思想》中关于构造器初始化以及访问权限控制的内容,上边的问题最根本的问题是因为AppConfig类放开了对象实例化的功能,构造器被设置为public域。换句话说,只要我们隐藏了这个方法,只由类来创造实例,那么外部就没有办法控制创建这个类的实例了。所以单例模式的实现方式就是首先收回创建实例的权限,然后通过类创建这个实例,并且提供一个可以获取实例的方法。
    在Java中,单例模式的实现分为两种(有的资料中说有三种,还有一种叫做登记式单例,实现较为复杂,暂且不考虑)一种叫做懒汉式单例模式,另一种叫做饿汉式单例模式。
    懒汉式单例模式示例代码:
package com.chenxyt.java.test;
public class Singleton{
 //定义一个存放单例对象的变量
 private Singleton uniqueInstance = null;
 //私有化构造函数,保证实例个数
 private Singleton(){
  //---处理业务,给对象的私有域赋值
 }
 //加锁保证线程安全 提供公共的获取实例方法 设置成静态方法,保证不用对象就可以调用
 public synchronized static Singleton getInstance(){
  //懒汉式设计,如果实例不存在则初始化
  if(uniqueInstance==null){
   uniqueInstance = new Singleton();
  }
  return uniqueInstance;
 }
}

    饿汉式单例模式示例代码:
package com.chenxyt.java.test;
public class Singleton{
 //定义一个存放实例的变量 
 private Singleton uniqueInstance = new Singleton();
 //私有化构造函数,保证实例个数
 private Singleton(){
  //---处理业务,给对象的私有域赋值
 }
 //提供公共的调用方法 由于只返回实例,所以不存在线程不安全问题
 public  Singleton getInstance(){
  return uniqueInstance;
 }
}

    所谓的懒汉式,就是唯一的类实例只有当马上要使用的时候才会创建,而饿汉式则是比较着急的那种,在类加载的时候就已经创建了该类实例。
    现在知道了单例设计模式,那么我们使用单例设计模式重写上边的示例代码。
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();
							}
					}
		}
	
}

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

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());
	}	
}

运行结果如下:

     Java常用的设计模式:单例模式_第3张图片
那么怎样验证单例模式呢?在《Java编程思想》中我们了解到了“==”运算符在比较两个对象的时候,实际上是比较两个对象的引用是否相同,也就是说当两个对象的引用相同的时候,这两个引用实际上是一个,指向同一个内存区域,也就是我们说的只有一个实例。那么我们测试下,在客户端代码中进行修改:
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("单例模式失败!我们是两个不同的实例!");
		}
	}	
}

运行结果如下:

     Java常用的设计模式:单例模式_第4张图片

三、模式讲解

    1.单例模式的功能:
    单例模式是用来保证程序运行过程中只会产生这一个实例,并且提供一个可以供全局访问的点也就是getInstance()方法来获取这个实例。单例模式只关系实例的创建方式,不涉及具体的业务场景。
    2.单例模式的作用范围:
    由于单例模式的原理是控制类实例的创建,因此它的作用范围在一个虚机上,因为类的加载过程是在虚机上执行的。所以我们讨论的单例模式只针对单一的系统,不讨论在集群上的情况。同时,通过Java的反射机制也可以创建类的实例,这种情况我们也不考虑,姑且暂认为没有反射机制。
    3.懒汉式的实现:
    前边我们写了懒汉式的实现示例代码,下面我们分析一下这段代码的设计思路。
    a.私有化构造方法:单例模式的核心思想就是收回实例创建的权限,改由类自己控制这样就能保证实例只有一个。因此首先要做的就是设置构造函数为私有:
private Singleton(){
}

     b.提供获取实例的方法:既然我们收回了创建实例的权限,那么我们就要为所有使用者提供一个统一的方法来获取实例,因此接下来就是提供一个全局方点来获取实例:

public Singleton getInstance(){
}
     c.把获取实例的方法变成静态:如上我们提供了一个全局的获取实例的方法,这时存在一个问题,这个方法是一个实例方法,也就是没有实例我们没办法调用,而我们恰恰是要通过这个方法获取实例,这时候就陷入了一个死循环。因此,我们可以通过将这个方法设置为static静态方法,然后直接通过类访问这个方法获取实例。static的作用我在前边文章专门学习过,附带一个传送门 Java编程思想学习笔记二(1):static关键字的四种用法
public static Singleton getInstance(){
}

    d.定义存储实例的属性:方法定义好了,实例怎么样创建呢?这里我们暂且忽略了线程安全的问题。如果我们直接返回一个new的实例可以不呢?答案显然是不可以的这样做跟直接new是一样的,反而比new更复杂了一些。所以我们要定义一个成员变量,然后返回这个成员变量,由于我们需要在一个static方法中使用这个变量,因此这个变量要被设置为static静态的。

private static Singleton instance = null;

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

public static Singleton getInstance(){
    if(instance==null){
        instance=new Singleton();
    }
    return instance;
}

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

懒汉式:
private static Singleton instance = null;

饿汉式:

private static Singleton instance = new Singleton();

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

    1.因为static变量在类加载的时候进行初始化,也就是只初始化一次!
    2.多个实例的static变量会共享同一块内存区域,实际上还是只用了一个!

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

四、单例模式的延迟加载

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

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

五、单例模式的缓存思想

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

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

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 map = new HashMap();
	//私有化构造函数,保证实例个数
	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.双重检查锁定
package com.chenxyt.java.test;
public class Singleton{
	//定义一个用来存储变量的值
	private static 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了,那么如果不判断最终还是会创建两个实例。

b.静态内部类
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构造器,这刚好符合了单例模式的思想。
package com.chenxyt.java.practice;
enum Singleton{
	uniqueInstance;
	public void doSomething(){
		//===
	}
}

八、总结

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

你可能感兴趣的:(设计模式)