设计模式-单例模式
单例模式
单例模式是最简单、同时也是使用最为广泛的设计模式之一。这种模式保证一个类只能拥有一个实例,并提供一个全局的访问点。值得注意的是,单例模式的实现必须满足三个必要条件,缺一不可:
- 单例类的构造函数必须使用private修饰。这可以保证该类无法从外部创建实例。
- 单例类中通过一个静态私有变量来存储唯一实例。
- 单例类中必须提供一个公开的静态方法,供外部使用者访问唯一实例。
单例模式的实现也有多种方式,其中最为人知的为饿汉式和懒汉式。
饿汉式
饿汉模式的典型实现代码如下:
public class HungrySingleton {
// 要素一:通过一个静态私有变量来存储该类的唯一实例。
private static final HungrySingleton instance = new HungrySingleton();
// 要素二:私有构造方法保证无法从外部创建该类的实例。
private HungrySingleton() {}
// 要素三:提供一个公开的静态方法,供外部使用者访问。
pulbic static HungrySingleton getInstance() {
return instance;
}
}
饿汉式如同它的名字一般,在类加载时就如同“饿汉”一样,创建该类的实例,并保存在静态私有变量中。由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。饿汉式天生就是线程安全的,所以我们无需考虑多线程环境下的线程安全问题。
懒汉式
饿汉模式好像很不错,既能满足单例模式的要求,而且也无需考虑线程安全问题,那为什么还有懒汉式呢?饿汉模式决定了即使后续没有用到,也会在类加载时创建实例。因此,会拖慢应用启动速度,浪费内存资源。
我想:这问题简单,那把创建实例的时机延缓到第一次使用时不就行了吗?于是写出了如下代码:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
上面的代码看似没问题,但有经验的开发者一眼就能看出,其实这段代码是线程不安全的,在多线程环境下可能会创建多个实例。知道了问题所在,解决起来也非常方便,我们可以使用Java提供的synchronized
关键字1包裹起来。于是,改进后的代码如下:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public synchronized static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
[1]
synchronized
:这是Java中使用的同步锁。当锁住的代码块内容执行完成或抛出异常时才会自动释放锁。而没有拿到锁的线程无法进入代码块执行,需要等待。该关键字可以标注在方法上、类上、或单独锁住一个代码块中的代码,这里只对标注在方法上的情况进行说明,其他的不再赘述。
若该关键字标注在普通方法上,例如:
public synchronized void test() {...}
实际上等价于:
public void test() { synchronized (this) { ... } }
也就是锁住了该类的对象。
若该关键字标注在静态方法上,例如:
public synchronized static void test(){...}
此时就不一样了,我们知道静态方法是属于类的,而不是某一个类的实例。因此这里加锁的对象就变成了当前类。上述代码块等价于:
// 当前静态方法在xxx类中 public static void test() { synchronized (xxx.class) { ... } }
改进后的代码满足了延迟加载的需求,也保证了线程安全,可以在多线程环境中使用。
双检锁
懒汉式已经很完美了,但是仍然有可以改进的地方。我们不难发现,加的同步锁似乎只会在实例创建时用到,而一旦实例创建完毕,也就不存在线程安全问题了。而后续其他使用者每次获取该类的实例,都会经过同步操作,加锁释放锁的操作是很重的,会显著影响性能,这把锁的存在成为了一个累赘。
为了解决这个问题,我们可以在原来的代码外面再包裹一层if判断,形成两层检查,比如这样:
public class LazySingleton {
private volatile static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized(LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
这样,当实例创建完成以后,外部的if判断就不通过了,因此不会执行同步代码块中的内容,规避了频繁加锁释放锁的性能问题。
同时我们似乎还发现了一个与之前的实现不一样的地方:静态属性上被添加了volatile
关键字。该关键字涉及到指令重排问题,待日后再来详细总结这方面的内容(TODO)。
其他单例模式实现方式
除了上述方式,还有些其他的方式也可以实现单例模式。
静态内部类实现
代码如下:
public class Singleton {
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
这种方式由JVM保证其在多线程环境下的线程安全。同时,这种实现方式看似是饿汉式的,实际上根据JVM虚拟机类初始化规则,只有当用到时才会被初始化。此部分涉及JVM虚拟机规范,待日后完善(TODO)。
枚举实现
枚举类写法简单,且无需考虑线程安全问题,也是一种不错的实现方式。
示例代码如下:
public enum SingletonEnum {
UNIQUE_INSTANCE("Unique Instance!");
// 下面可以当成一个正常的类来使用
private String name;
SingletonEnum(String name) {
this.name = name;
}
public void print() {
System.out.println(STR."My name is \{name}");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public static void main(String[] args) {
SingletonEnum.UNIQUE_INSTANCE.print();
}
}