单例模式应该是大家最为熟知的一种设计模式了,相信大家或多或少的都在自己的项目中使用过单例模式,例如封装一个Log工具类、一个数据库存取类或者用户登录管理类等。而我们使用单例模式主要有两个目的:
- 减少内存消耗
- 保证某些共享资源的唯一性
单例模式的写法有好多种,如:饿汉式单例模式、懒汉式单例模式、IoDH单例模式、枚举式单例模式,下面就来一一实现下这些写法。
饿汉式单例模式
public class HungrySingletonV1 {
private static HungrySingletonV1 instance = new HungrySingletonV1();
private HungrySingletonV1(){
}
public static HungrySingletonV1 getInstance(){
return instance;
}
}
上面的例子中,类被加载时,静态变量instance就会被初始化,这时候单例类的唯一实例就被创建了。
- 优点:类加载时就已经实例化,避免了线程问题
- 缺点:由于类加载的时候就实例化了,没有达到懒加载的效果,可能会造成内存浪费
饿汉式单例模式还要一种变种
public class HungrySingletonV2 {
private static HungrySingletonV2 instance;
static {
instance = new HungrySingletonV2();
}
private HungrySingletonV2(){
}
public static HungrySingletonV2 getInstance(){
return instance;
}
}
效果与前面的一样,不过把初始化方法放到静态代码块中,也是在类加载时调用。
懒汉式单例模式
低配版
public class LazySingletonV1 {
private static LazySingletonV1 instance ;
private LazySingletonV1(){
}
public static LazySingletonV1 getInstance(){
if(instance == null){
instance = new LazySingletonV1();
}
return instance;
}
}
上面的代码是最简单的懒汉式单例模式,实现懒加载,每次获取实例时会去判断是否已创建实例,如果一直没人用,则不用创建实例,节省内存空间。
但实际上,上面这种写法应该是最不推荐的一种单例模式写法。因为它是线程不安全的,如果多个线程同时获取该实例,就会创建多个实例对象,不符合单例的需求。
进阶版
public class LazySingletonV2 {
private static LazySingletonV2 instance ;
private LazySingletonV2(){
}
public static synchronized LazySingletonV2 getInstance(){
if(instance == null){
instance = new LazySingletonV2();
}
return instance;
}
}
可以看到在这种懒汉式单例模式中,我们在获取实例的方法上加了一个同步锁,这样保证了获取实例的方法在不同线程中是同步的,使得获取的实例是唯一的。
但是这种写法有一个最大的问题就是,效率太低。每一个线程都要进行等待,而实际中,如果已经创建了,后面的想获取实例,直接返回就行。
再进阶版
public class LazySingletonV3 {
private volatile static LazySingletonV3 instance;
private LazySingletonV3() {
}
public static LazySingletonV3 getInstance() {
if (instance == null) {
synchronized (LazySingletonV3.class) {
instance = new LazySingletonV3();
}
}
return instance;
}
}
基于前一个版本的问题,有人想到了将同步锁置于instance为空判断之后,这样就实现了当实例已经创建,后面的获取时直接返回的问题。
但是,这种写法又导致了线程不安全。如果A用户拿到了同步锁,正在创建实例,另一个B用户在实例还未创建时到了同步锁外等候,当A用户创建完实例,退出同步锁后,B用户马上就获取了同步锁并开始创建实例,这就导致了创建了多个实例。
终极版
//双重校验锁
public class LazySingletonV4 {
private volatile static LazySingletonV4 instance;
private LazySingletonV4(){
}
public static LazySingletonV4 getInstance(){
//检查实例是否存在,不存在才进入同步块
if(instance == null){
//同步块,保证线程安全
synchronized(LazySingletonV4.class){
//再次检查实例是否存在,不存在才创建实例
if(instance == null){
instance = new LazySingletonV4();
}
}
}
return instance;
}
}
双重校验算是懒汉式单例模式的终极版本了,先判断实例是否为空,为空获取同步锁,在同步锁内再判断一次实例是否为空,既保证了线程安全又提高了效率。
双重校验加锁的实现一般会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
IoDH单例模式
什么是IoDH呢?
IoDH是Initialization Demand Holder 的缩写,简单来说就是在单例类中增加一个静态内部类,在该内部类中创建单例类的实例。
我们知道,在多线程开发时,为了解决并发问题,我们会使用synchronized来加互斥锁进行同步控制。但是在某些情况下,JVM已经隐含为您执行了同步,这些时候就不需要自己进行同步控制了。这些情况包括:
- 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
- 访问final字段时
- 在创建线程之前创建对象时
- 线程可以看见它将要处理的对象时
例如饿汉式单例模式,就是在类加载时进行了初始化,也就是由静态初始化器初始化的。但是饿汉式单例模式不符合懒加载的要求,如果可以让类加载时不去初始化对象,不就解决问题了吗。这就是IoDH的方法,通过定义一个静态内部类,在这个静态内部类中创建单例类实例,当我们需要使用时才会去加载这个静态内部类,创建单例类实例。
public class IoDHSingleton {
private IoDHSingleton() {
}
private static class SingletonHolder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static IoDHSingleton instance = new IoDHSingleton();
}
public static IoDHSingleton getInstance() {
return SingletonHolder.instance;
}
}
当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建IoDHSingleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
枚举式单例模式
借助JDK1.5中添加的枚举来实现单例模式,应该是最好的实现单例模式的方式,代码也很简单。
public enum EnumSingleton {
instance;
public void method(){
//功能方法
}
}
访问也很简单,通过EnumSingleton.instance即可调用枚举类中的方法了。这种单例模式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
总结
在实际项目中 饿汉式单例模式、懒汉式单例模式终极版、IoDH单例模式、枚举式单例模式 都是可以选择的实现方式,看个人喜好。不过枚举式单例模式应该是最简单且安全的实现方式,推荐使用。