设计模式(二)单例

142 阅读5分钟

单例模式是指一个类确保其只拥有一个实例对象,且此实例对象由类自己创建并提供方法为整个系统提供这个实例的访问。

一、特点

  • 实现单例模式的类只有唯一一个实例对象
  • 实现单例模式的类必须自己创建此实例对象
  • 实现单例模式的类必须向整个系统提供访问此实例对象的方法

二、代码实现

1. 饿汉式单例

单例类在加载时即实例化唯一对象。

package singleton;

public class HungrySingleton {
    
    private static final HungrySingleton singleton = new HungrySingleton();
    
    private HungrySingleton() {}
    
    public static HungrySingleton getSingleton() {
        return singleton;
    }
}

2. 懒汉式单例

单例类在系统首次调用其提供的访问实例对象的方法时实例化唯一对象。

package singleton;

import java.util.Objects;

public class LazySingleton {

    private static LazySingleton singleton;

    private LazySingleton() {}

    public static LazySingleton getSingleton() {
        if (Objects.isNull(singleton)) {
            singleton = new LazySingleton();
        }
        return singleton;
    }
}

相比于饿汉式单例,懒汉式单例更加合理,只有需要时才创建具体的实例对象而非类加载时便实例化此对象。为说明此问题请看以下代码:

package singleton;

public class HungrySingleton {
    
    private static final HungrySingleton singleton = new HungrySingleton();
    
    private HungrySingleton() {}
    
    public static HungrySingleton getSingleton() {
        return singleton;
    }
    
    public static void methodA() {
        System.out.println("Provide other service");
    }
}

在实现饿汉式单例的类中新增了一个方法methodA,该方法可以提供服务,当methodA被首次调用时,HungrySingleton便被加载并实例化了静态属性singleton,但实际上此实例对象尚未被使用,即使后续系统中一直不使用singleton实例对象,此实例对象也会常驻内存。

3. 加锁实现懒汉式单例

第 2 部分懒汉式单例代码中存在一个问题,没有考虑多线程并发场景,当多个线程同时访问getSingleton方法时可能实例化多个对象,但最终只有一个对象被赋给静态属性singleton,其它对象只能等待垃圾回收,这不免造成了系统资源浪费。处理线程并发问题最简单的方法是通过加锁实现线程同步。

package singleton;

import java.util.Objects;

public class LazySingleton {

    private static LazySingleton singleton;

    private LazySingleton() {}

    public static synchronized LazySingleton getSingleton() {
        if (Objects.isNull(singleton)) {
            singleton = new LazySingleton();
        }
        return singleton;
    }
}

以上代码中对懒汉式单例类提供的访问唯一对象实例的方法添加了一个内部锁(当然可以将synchronized同步块加在方法体内,或使用显示锁方案替换内部锁),这样就保证同时只有一个线程能够访问getSingleton方法,就不可能出现多个线程同时实例化对象的场景。

4. 双重锁单例

加锁实现懒汉式单例虽然解决了线程同步问题,但还存在缺陷,即singleton在实例化成功后,后续如果有多个线程同时调用getSingleton方法试图获取singleton的引用,这些线程仍需串行执行,由此造成性能瓶颈。解决此问题的方法是采用双重锁单例。

package singleton;

import java.util.Objects;

public class LazySingleton {
    
    private static LazySingleton singleton;
    
    private LazySingleton() {}
    
    public static LazySingleton getSingleton() {
        if (Objects.isNull(singleton)) {
            synchronized (LazySingleton.class) {
                if (Objects.isNull(singleton)) {
                    singleton = new LazySingleton();
                }
            }
        }
        return singleton;
    }
}

双重锁解决了多线程获取单例对象的性能问题,只有对象第一次实例化时会使用线程同步,一旦实例化成功,后续调用getSingleton获取对象实例都无需再次同步执行。

5. 增加关键字volatile

双重锁单例仍然存在发生异常的可能。原因是,为了提升执行效率,Java 虚拟机可能会对synchronized块中代码进行重排序。虽然singleton = new LazySingleton();看似一个语句,实际执行过程中会分为几个步骤完成:

  1. 分配内存区域
  2. 在已分配内存区域进行对象实例化
  3. 将实例化对象引用赋值给singleton

Java 虚拟机在执行过程中可能会对上诉步骤 2 和 3 进行重排序,即先将内存区域引用赋值给singleton然后再实现对象实例化,如果在对象实例化前一个线程调用getSingleton方法,则此时singleton不为null,但实际上对象实例化并未完成,该线程可能会拿到一个未实例化完的对象引用调用其提供的实例方法,很明显因为对象并未完成实例化,所以此时的实例方法调用肯定会出现异常。

为解决此问题,可以在静态属性singleton上加上关键字volatile,这会阻止 Java 虚拟机的重排序行为,保证对象是先实例化后再赋值给singleton

package singleton;

import java.util.Objects;

public class LazySingleton {
    
    private static volatile LazySingleton singleton;
    
    private LazySingleton() {}
    
    public static LazySingleton getSingleton() {
        if (Objects.isNull(singleton)) {
            synchronized (LazySingleton.class) {
                if (Objects.isNull(singleton)) {
                    singleton = new LazySingleton();
                }
            }
        }
        return singleton;
    }
}

6. 使用一个局部变量实例化对象,然后将此局部变量赋值给 singleton

除使用关键字volatile外,还可以通过局部变量实例化对象赋值的方法解决指令重排带来的问题。

package singleton;

import java.util.Objects;

public class LazySingleton {
    
    private static LazySingleton singleton;
    
    private LazySingleton() {}
    
    public static LazySingleton getSingleton() {
        if (Objects.isNull(singleton)) {
            synchronized (LazySingleton.class) {
                if (Objects.isNull(singleton)) {
                    LazySingleton temp = new LazySingleton();
                    singleton = temp;
                }
            }
        }
        return singleton;
    }
}

7. 使用静态内部类实现懒汉式单例

还有一种更加简单的单例实现方法,利用了 Java 静态内部类的机制,当 LazySingleton 被加载时并不会触发其静态内部类 Holder 的加载,只有首次调用 getSingleton 方法时才会加载静态内部类 Holder,又因 Holder 的静态属性 singletonfinal,所以只会被实例化一次。

package singleton;

public class LazySingleton {
    
    private LazySingleton() {}
    
    private static final class Holder {
        private static final LazySingleton singleton = new LazySingleton();
    }
    
    public static LazySingleton getSingleton() {
        return Holder.singleton;
    }
}