Java SE - 单例模式详解
单例模式的概念
作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。
单例模式的特点
- 作为单例类,只能有一个实例存在;
- 作为单例类,必须自己创建自己唯一的实例;
- 作为单例类,必须要有一个方法为外界提供这一实例。
单例模式的分类
单例模式大致分为两种类型:
- 饿汉模式 - 在类加载的时候就创建自己的实例,无论使用还是未使用。
- 懒汉模式 - 在类加载的时候仅仅声明一个当前类的变量而不去实例化,当真正要使用的时候才会去实例化这个对象。
- 登记薄模式 - 使用Map集合(类似于登记薄)保存创建的各种类的实例,如果某个类创建过实例(已经登记过),则直接从工厂返回这个实例;如若没有,则先放入Map集合中(去登记),再返回这个实例。
这里只介绍前两者和一些优化方式。
饿汉模式
顾名思义,“饿汉”这个词一看就能想到穷,一个人大冬天的,看着别人家糊着烤鸡,在窗外站着却进不去,非常着急。所以,回到程序中,在加载类的时候就要实例化当前类的对象了。
/**
* 饿汉类型的单例模式
*
* @author Steve Jrong
*
*/
public class EagerSingleton {
/*
* 饿汉模式(创建实例比较着急):静态的EagerSingleton类型的变量instance属于类范围内的变量,所以在类加载的时候就已经创建了当前类的实例了
* 他不管你用不用,先创建了类的实例再说。
*/
private static EagerSingleton instance = new EagerSingleton();
/**
* 私有的默认构造子
*/
private EagerSingleton() {
}
/**
* 静态的工厂方法(Factory Method)
*
* @return
*/
public static EagerSingleton getInstance() {
return instance;
}
}
在类范围内定义一个静态的当前类类型的对象并立即实例化。
之后使用静态工厂获取实例即可。
懒汉模式
顾名思义,“懒汉”这个词一看就能想到邋遢,不勤快,干什么都拖拖拉拉的。回到程序中,就表示创建自身的实例并不那么着急,不需要在加载类的时候就马上创建实例,而是等到真正要用的时候才去创建实例(懒人都这样,拖不下去了才肯硬着头皮开干)。
package org.stevejrong.singleton;
/**
* 懒汉类型的单例模式
*
* @author Steve Jrong
*
*/
public class LazySingleton {
/*
* 懒汉模式(创建实例不着急):静态的LazySingleton类型的变量instance虽说属于类范围内的变量,但并没有在加载类时立即创建实例
* ,而是当你真正用到它的时候才会创建实例。
*/
private static LazySingleton instance = null;
/**
* 私有的默认构造子
*/
public LazySingleton() {
}
/**
* 静态的工厂方法(Factory Method)
*
* @return
*/
/*
* 这里使用synchronized(同步)关键字来处理多线程环境下的情况,防止因多线程下创建多个当前类的实例
*/
public static synchronized LazySingleton getInstance() {
// 当instance为空时才创建实例,否则直接返回已存在的实例
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
懒汉模式仍然需要在类范围内定义自身类类型的变量,但注意在这里不立即实例化,而是:
将工厂方法加同步锁并在方法内判断参数传入的自身类的实例是否为空。如果为空就创建,不为空则直接返回。
这里加同步锁目的是为了处理在多线程环境下避免同时有多个线程同时访问工厂创建多个当前类的实例。
明显缺点
饿汉模式的缺点:加载类的时候就创建实例,浪费了宝贵的内存空间。
懒汉模式的缺点:使用了同步锁,那么一定是线程安全的,但会降低执行效率;而且每次都需要判断,也会降低执行效率。
新型方式:基于双重检查加锁的单例模式
所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,
进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块【第一重检查】
进入同步块【加锁】之后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例【第二重检查】
这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
“双重检查加锁”机制的实现会使用关键字volatile。
被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
package org.stevejrong.singleton;
/**
* 双重检查加锁类型的单例模式
*
* @author Steve Jrong
*
*/
public class DualCheckLockSingleton {
private static volatile DualCheckLockSingleton instance = null;
/**
* 私有构造子
*/
public DualCheckLockSingleton() {
}
public static DualCheckLockSingleton getInstance() {
// 【一次检查】首先判断是否存在当前类的实例,没有才会执行if中的语句
if (instance == null) {
// 【同步锁】使用同步锁处理多线程环境下创建实例的情况
synchronized (DualCheckLockSingleton.class) {
// 【二次检查】再次检查来确认是否存在当前类的实例
if (instance == null) {
// 没有,则创建一个实例并返回
instance = new DualCheckLockSingleton();
}
}
}
return instance;
}
}
仍然在类范围内定义一个当前类类型的静态变量,但不实例化。
此时无需让整个工厂方法同步了,他只是一个普通的静态方法:
第一次检查实力是否被创建,如若没有创建过,才会执行上了同步锁的内部类,来获得真正的实例。
缺点
由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别需要,尽量不要使用。
鱼与熊掌兼得之法:基于Lazy initialization holder class的单例模式
此种方法使用了Java SE中类级内部类和多线程下缺省同步锁的知识。
类级内部类简介:
类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。
JVM中默认添加同步锁的情形:
- 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时;
- 访问final字段时;
- 在创建线程之前创建对象时;
- 线程可以看见它将要处理的对象时。
package org.stevejrong.singleton;
/**
* 懒加载类型的单例模式
*
* @author Steve Jrong
*
*/
public class LazyInitHolderSingleton {
/**
* 私有构造子
*/
public LazyInitHolderSingleton() {
}
/**
* 类级内部类
*
* @author Steve Jrong
*
*/
/*
* 类级内部类:在外部类中定义一个静态类型的内部类,这样的话外部类和内部类就没有绑定关系了,即使初始化了外部类,内部类也不会跟着一起实例化,
* 只有在访问类级内部类时,此内部类才会被实例化。这样就能达到懒加载的效果了。
*
* 这个instance不需要显式地指定同步锁,因为JVM会自动为此种情况上锁来满足多线程情况下的需求
*/
private static class LazyInitHolder {
private static LazyInitHolderSingleton instance = new LazyInitHolderSingleton();
}
/**
* 静态的工厂方法(Factory Method)
*
* @return
*/
/*
* 注意,此方法并没有像饿汉模式和懒汉模式那样使用同步锁,这样可以节省运行时间,提高效率
*/
public static LazyInitHolderSingleton getInstance() {
return LazyInitHolder.instance;
}
}
此时就无需在类范围内定义自身类的变量了,而是巧妙地在工厂方法中调用类级内部类中定义的当前类类型的静态变量并直接返回。
由于是类级内部类,属于静态范围的类,所以JVM只会初始化一次,所以这一块的话不需要开发者显式地上同步锁,JVM会自己搞定。
优点
在代码中既没有显式地指定同步,提高了执行效率,又可以达到线程安全的标准,所谓两全其美之法。
优化
可以使用Enum来代替上面繁琐冗长的代码,
使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化机制,并由JVM从根本上提供保障,绝对防止多次实例化,是更简洁、高效、安全的实现单例的方式。
package org.stevejrong.singleton;
/**
* 枚举单例模式
*
* @author Steve Jrong
*
*/
public enum EnumSingleton {
singleInstance;
public EnumSingleton getInstance() {
return singleInstance;
}
}
最后提供main()方法和每种单例模式的执行时间:
package org.stevejrong.test;
import org.stevejrong.singleton.DualCheckLockSingleton;
import org.stevejrong.singleton.EagerSingleton;
import org.stevejrong.singleton.EnumSingleton;
import org.stevejrong.singleton.LazyInitHolderSingleton;
import org.stevejrong.singleton.LazySingleton;
public class Test {
static long startTime;
public static void main(String[] args) {
System.out
.println("---------------------------------饿汉模式测试----------------------------------");
startTime = System.nanoTime();
EagerSingleton eagerSingleton1 = EagerSingleton.getInstance();
System.out.println("第一次实例化" + eagerSingleton1.getClass().getName()
+ "类的实例 HashCode:" + eagerSingleton1.getClass().hashCode());
EagerSingleton eagerSingleton2 = EagerSingleton.getInstance();
System.out.println("第二次实例化" + eagerSingleton2.getClass().getName()
+ "类的实例 HashCode:" + eagerSingleton2.getClass().hashCode());
System.out.println("用时:" + (System.nanoTime() - startTime));
System.out
.println("---------------------------------懒汉模式测试----------------------------------");
startTime = System.nanoTime();
LazySingleton lazySingleton1 = LazySingleton.getInstance();
System.out.println("第一次实例化" + lazySingleton1.getClass().getName()
+ "类的实例 HashCode:" + lazySingleton1.getClass().hashCode());
LazySingleton lazySingleton2 = LazySingleton.getInstance();
System.out.println("第二次实例化" + lazySingleton2.getClass().getName()
+ "类的实例 HashCode:" + lazySingleton2.getClass().hashCode());
System.out.println("用时:" + (System.nanoTime() - startTime));
System.out
.println("---------------------------------双重检查加锁模式测试----------------------------------");
startTime = System.nanoTime();
DualCheckLockSingleton dualCheckLockSingleton1 = DualCheckLockSingleton
.getInstance();
System.out.println("第一次实例化"
+ dualCheckLockSingleton1.getClass().getName()
+ "类的实例 HashCode:"
+ dualCheckLockSingleton1.getClass().hashCode());
DualCheckLockSingleton dualCheckLockSingleton2 = DualCheckLockSingleton
.getInstance();
System.out.println("第二次实例化"
+ dualCheckLockSingleton2.getClass().getName()
+ "类的实例 HashCode:"
+ dualCheckLockSingleton2.getClass().hashCode());
System.out.println("用时:" + (System.nanoTime() - startTime));
System.out
.println("---------------------------------懒加载模式测试----------------------------------");
startTime = System.nanoTime();
LazyInitHolderSingleton lazyInitHolderSingleton1 = LazyInitHolderSingleton
.getInstance();
System.out.println("第一次实例化"
+ lazyInitHolderSingleton1.getClass().getName()
+ "类的实例 HashCode:"
+ lazyInitHolderSingleton1.getClass().hashCode());
LazyInitHolderSingleton lazyInitHolderSingleton2 = LazyInitHolderSingleton
.getInstance();
System.out.println("第二次实例化"
+ lazyInitHolderSingleton2.getClass().getName()
+ "类的实例 HashCode:"
+ lazyInitHolderSingleton2.getClass().hashCode());
System.out.println("用时:" + (System.nanoTime() - startTime));
System.out
.println("---------------------------------单例枚举模式测试----------------------------------");
startTime = System.nanoTime();
System.out.println("第一次实例化枚举实例 HashCode:"
+ EnumSingleton.singleInstance.getClass().hashCode());
System.out.println("第二次实例化枚举实例 HashCode:"
+ EnumSingleton.singleInstance.getClass().hashCode());
System.out.println("用时:" + (System.nanoTime() - startTime));
}
}
Console: