单例模式也是一种创建型模式,而且也非常容易理解:在一个系统中可能需要多个配置文件,我们希望这些配置文件的实例只存在一个,而不是存在多个重复的实例。这时候就需要使用单例模式。
单例模式有几个要点:
- 一是必须确保只存在一个类的实例。
- 二是类必须自己创建自己,不允许其他类来创建自己。
- 三是必须提供一个方法允许其他类访问单例成员。
根据这些特点,我们可以很容易猜出单例类在Java的样子:首先他的构造方法必须是私有的,然后往往需要一个公有的静态方法获取单例实例。
单例的实现
单例模式的实现有很多种,按照单例的实例化的时机可以分为饿汉式和懒汉式两种,下面来逐一说明。
懒汉式(非线程安全)
这种方式非常简单,也很容易理解。单例实例在第一次调用的时候才创建,符合懒加载的要求。唯一缺点是这种方式不支持多线程,在多线程环境下可能会创建多个对象。
public class UnThreadSafeSingleton {
private UnThreadSafeSingleton() {
}
private static UnThreadSafeSingleton singleton;
public static UnThreadSafeSingleton getSingleton() {
if (singleton == null) {
singleton = new UnThreadSafeSingleton();
}
return singleton;
}
}
懒汉式(同步的)
我们可以对上面的实现方式进行改进,以便在多线程环境下也可以正常工作。实现方式很简单,直接在方法上添加synchronized
关键字即可。
这种实现方式虽然也很简单,但是性能不咋地。由于直接在方法上加了锁,所以如果同时有两个地方获取单例对象,其中一个就会阻塞。在获取单例的次数获取比较多的时候性能很差。
public class SynchronizedThreadSafeSingleton {
private static SynchronizedThreadSafeSingleton singleton;
private SynchronizedThreadSafeSingleton() {
}
public synchronized static SynchronizedThreadSafeSingleton getSingleton() {
if (singleton == null) {
singleton = new SynchronizedThreadSafeSingleton();
}
return singleton;
}
}
饿汉式(静态初始化)
如果不要求必须懒加载,那么我们可以使用JVM的类加载工作机制,方便的实现单例模式。
JVM在第一次加载类的时候,会被初始化累的静态域,并确保静态域只初始化一次。所以我们可以将创建单例的代码放到静态初始化块中,这样JVM会帮我们创建单例。这种方式的缺点就是加载类的时候就创建了单例对象,没有懒加载。
public class FirstLoadSingleton {
private static FirstLoadSingleton singleton;
private FirstLoadSingleton() {
}
static {
singleton = new FirstLoadSingleton();
}
public static FirstLoadSingleton getSingleton() {
return singleton;
}
}
双检锁方式
这种方式比较复杂,但是其他方面都很好:既实现了懒加载,同时也是线程安全的,性能还不错。
双检锁模式的要点:一是单例必须使用volatile
关键字标记;二是在创建单例的时候要进行两次检查(这就是双检锁的含义)。我们可以看到同步块在第一次判断之后,也就是说只有在第一次调用时才可能发生竞争和阻塞。单例创建之后,在获取单例的时候不会调用同步块,因此速度会非常快。和前面的直接在方法上添加同步的例子相比,真是不知道高到哪里去了。
public class DoubleCheckLockSingleton {
private volatile static DoubleCheckLockSingleton singleton;
private DoubleCheckLockSingleton() {
}
public static DoubleCheckLockSingleton getSingleton() {
if (singleton == null) {
synchronized (DoubleCheckLockSingleton.class) {
if (singleton == null) {
singleton = new DoubleCheckLockSingleton();
}
}
}
return singleton;
}
}
静态内部类方式
这种方式和双检锁方式的效果类似,既可以保证懒加载又具有多线程下的性能优势。而且实现起来更加简单。唯一缺点就是单例对象必须是静态的,而双检锁方式的单例对象可以是实例的。
道理也很简单,如果我们把单例放到类的静态字段上,不能保证延迟加载的话,那么再用一层内部类包住不就行了。这样,当外层类第一次加载的时候,不会触发单例的初始化。而在第一次获取单例的时候,才会调用内部类,从而让JVM加载单例。
public class InnerClassSingleton {
private static class Inner {
private static InnerClassSingleton singleton = new InnerClassSingleton();
}
private InnerClassSingleton() {
}
public static InnerClassSingleton getSingleton() {
return Inner.singleton;
}
}
枚举方式
这种方式是Java实现单例最好的方式,连《Effective Java》都推荐我们使用这种方式。不过现在貌似使用的还是比较少。一来,枚举是Java 1.5才加入的东西;二来,Java的枚举使用起来确实很捉急。甚至有些开发实践都要求不使用枚举,而是使用共有静态字段来代替。所以枚举单例这种方式就比较稀少了。
不过确实,Java的枚举天生就是为实现单例而存在的。首先,枚举的实例是在使用时才被初始化的,这和单例模式延迟加载的要求相符。其次,枚举类型只允许存在私有的构造函数,从根本上杜绝了创建多个单例的可能性。而且当枚举序列化和反序列化的时候,同样会保证单例的唯一性。因此我们说,枚举方式是Java实现单例最好的方式。
可能还是不太好理解,所以还是直接看代码吧。假设我们需要一个单例的配置对象,我们可以创建枚举来解决。枚举的构造方法默认(且只能)是私有的,我们直接在构造方法中初始化数据(例如从文件读取等等),然后通过枚举类中定义的方法来读取数据。如果对Java的枚举还是感觉到比较陌生的话回去复习一下枚举类的用法。
public enum EnumSingleton {
Instance;
private String data;
EnumSingleton() {
//在构造方法中进行初始化
data = "Some data";
}
public String getData() {
return data;
}
}
当然在现在的Java生态中单例模式一般不需要我们手动实现了。像Spring和Guice这样的依赖注入框架已经实现了单例模式,所以我们在使用这些框架的时候,创建和确保单例的工作有这些框架完成,我们只需要编写传统的非线程安全类即可。