Fork me on GitHub

设计模式之单例模式

在实际的项目开发中,常会用到配置文件,可在读取配置文件后将读取的内容放在数据对象中,但在使用时通过new的方式产生对象,在系统中会存在多个相同的配置文件,当配置文件过多时会极大的影响到系统的性能。我们是否能使得配置文件的实例对象在系统运行期间只有一个,以避免了上述问题的发生,这就是我们今天说的单例模式。

一、单例模式实现

单例模式也称单件、单体等,单例模式保证一个类只有一个实例,并提供一个全局访问点。一个类能够创建多个实例的根源是类的构造方法是公开的,因此,要控制类的创建,首先就要收回创建类实例的权限,同时提供可供外部访问类实例的方法。

单例模式的结构图如下所示:

单例模式的实现方式分为两种:懒汉式和饿汉式,他们主要是在创建实例对象的处理方式不同。下面分别对两种方式进行实现:

  • 饿汉式单例
public class Student {
    // 成员变量初始化本身对象
	private static Student student = new Student();
    
    // 1:构造私有,内部控制实例数
	private Student() {
	}

	// 2:对外提供公共方法获取对象
	public static Student getInstance() {
		return student;
	}
}
  • 懒汉式单例
public class Student {

	private Student() {
	}
	
	//此处使用一个内部类来维护单例 JVM在类加载的时候,是互斥的,所以可以由此保证线程安全问题
	private static class SingletonFactory {
		private static Student student = new Student();
	}
	/* 获取实例 */
	public static Student getInstance() {
		return SingletonFactory.student;
	}
}

懒汉与饿汉是一种形象的称谓,饿汉是在装载类时创建对象实例,而懒汉式在使用 的时候才进行对象的创建。

二、单例模式说明

单例模式的本质是控制实例数目。单例模式保证类运行期间只有一个实例对象,并提供一个全局访问点,即示例代码中的getInstance方法。Java中实现的单例的范围是一个虚拟机,虚拟机通过自己的ClassLoader装载饿汉式实现单例类创建类实例。

单例模式调用的示意图如下所示:

懒汉式调用顺序:

饿汉式调用顺序:

单例模式的懒汉加载方式体现了延迟加载的思想。只有当使用资源或数据时才进行加载,称为“lazy load”,起到尽可能节约资源的效果。

此外,懒汉式加载模式还体现了缓存的思想,即当数据或资源被频繁访问时,将数据魂村到内存中,每次先到内存中查找数据,有则使用,无则获取,从而节约时间,这也是一种典型的空间换时间的方案

1、单例模式的线程安全

饿汉式是线程安全的,而不加同步的懒汉式加载是线程不安全的,当A,B两个线程同时进入getInstance方法,当A进行实例是否存在判断时,B线程已经开始进行实例的创建,程序运行,A线程也会进行实例的创建,会创建出两个实例,这里的单例在并发情况下失效。

下面为不加同步的懒汉式加载代码:

public class Student {
	private Student() {
	}

	private static Student student = null;
	
	public static Student getInstance() {
        if(student == null){
            student = new Student();
        }
		return student;
	}
}

而懒汉式的也可以实现线程安全,可通过给getInstance方法添加synchronized关键字实现,但会降低访问速度。

此外,也可以通过双重检查加锁的方式实现的,不仅能够保证线程安全,也不会对性能产生大的影响。双重检查锁机制是指进入方法后先检查实例是否存在,这是第一重判断,若实例不存在进入同步代码块。在同步代码块中检查实例是否存在,若不存在则创建实例,这是第二重判断。

双重检查锁机制使用关键字volatile,被其修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程正确处理该变量。示例代码如下:

public class Student {
	private Student() {
	}
	//实例变量volatile修饰
	private volatile static Student student = null;
	
	public static Student getInstance() {
        //检查实例是否存在
        if(student == null){
            //同步块,线程安全创建实例
            synchronized(Student.class){
                //检查实例是否存在,不存在则创建
                if(student == null){
                    student = new Student();
                }
            }
        }
		return student;
	}
}

注意:

volatile关键字可能屏蔽掉虚拟机中必要的优化代码,且效率不高,因而没有特别需求,一般不使用。

2、Java单例模式实现

上面的单例模式实现存在缺陷,在Java中有一种Lazy initlialization holder calss模式,使用Java内部类和多线程缺省同步锁实现类延迟加载与线程安全。

类级内部类指有static修饰的成员式内部类,无static修饰的称为对象级内部类,他的特点如下:

  • 类级内部类与外部类中的static修饰的相似,与外部类对象无依赖关系。而对象级内部类与外部类实例对象绑定。
  • 类级内部类中可定义静态方法,静态方法中只能引用外部类中的静态成员方法或成员变量。
  • 类级内部类相当于外部类的成员,只在第一次使用时被装载。

Java开发中主要通过synchronized加互斥锁进行同步控制,但在某些情况中,JVM已经隐含的执行了同步,其中就包括由静态初始化器初始化数据时,可采用这种方式由JVM保证线程的安全性。其实力代码如下:

public class Student {

	private Student() {
	}
	
	//类的内部类,即静态成员内部类,它只有被调用的时在会装载,从而实现延迟加载
	private static class SingletonFactory {
        //静态初始化器,由JVM保证线程安全
		private static Student student = new Student();
	}
	/* 获取实例 */
	public static Student getInstance() {
		return SingletonFactory.student;
	}
}

即我们在模式实现时给出的示例代码。

3、枚举单例模式实现

单元素的枚举类型也可以进行单例模式的实现,枚举有一些特点:

  • Java的枚举类型是功能齐全的类型,可以有自己的属性和方法。
  • Java的枚举类型基本思想是通过公有的静态final域为枚举常量导出实例。
  • 枚举是单例的泛型化,单例的本质是单元素枚举。

枚举实现单例的代码如下:

public enum Singleton{
    //枚举元素
    uniqueInstance;
    
    public void singletonOperation(){
        //功能实现
    }
}

三、单例模式总结

1、适用场景

需要控制类的实例只能有一个,且客户端能从全局访问点访问实例。

2、模式优缺点

  • 时间与空间。懒汉式是典型的时间换空间,饿汉式是典型的空间换时间。懒汉式浪费运行时间进行是否创建的判断,饿汉式是先创建,调用不在进行判断,节约运行时间。

  • 线程安全。饿汉式是线程安全的,而不加同步的懒汉式加载是线程不安全的,但可通过关键字synchronized或volatile将懒汉式加载变为线程安全的。此外,Java实现了线程安全与延迟加载兼顾的单例模式实现方式。

参考:文章主要参考《研磨设计模式》一书

posted @ 2019-01-31 22:09  紫焱luis  阅读(156)  评论(0编辑  收藏  举报