在实际的项目开发中,常会用到配置文件,可在读取配置文件后将读取的内容放在数据对象中,但在使用时通过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实现了线程安全与延迟加载兼顾的单例模式实现方式。
参考:文章主要参考《研磨设计模式》一书