设计模式
单例模式
在Java编程中,有时候为了节省内存和计算,某些类的实例只需要初始化一次,这时就需要使用单例模式。下面介绍单例模式的几种常见写法。
饿汉式
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {} // 私有化构造器,防止被外部调用实例化对象。
public static Singleton getInstance() {
return INSTANCE;
}
}
这种方式写法简单,在类进行加载时就完成实例化,避免了线程同步的问题。缺陷是:没有lazy loading的效果,会造成内存的浪费。
懒汉式-线程不安全
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方式起到了Lazy loading的效果,但是只能在单线程下使用,如果是多线程,那么
if (instance == null)
判断语句块可能会出现问题。
懒汉式-synchronized方式
public class Singleton {
private static Singleton instance;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
使用
synchronized
锁住获取实例的方法,虽然线程安全,但是效率非常低,因为并发条件下,getInstance()
方法不能被同步调用。
懒汉式-双重检查锁定方式
由于上述加锁方式导致的效率低下,因此想到了使用双重检查的锁定方式,如下是有问题的代码:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Singleton(); // 问题根源
}
}
}
return instance;
}
}
多个线程创建对象时,会通过加锁的方式保证只有一个线程能创建成功,创建好对象之后,则不需要加锁。看似完美,却可能出现没有完全初始化的问题。
问题出现原因分析:
上面初始化对象的一行代码,可以分解为下列三行伪代码:
memory = allocate(); // 1:分配对象的内存空间 ctorInstance(memory); // 2:初始化对象 instance =memory; // 3:设置 instance 指向刚分配的内存地址
在单线程中,上面的代码2和3之间可能会被重排序,重排序后并不会影响最终结果并且可以提高程序执行性能。
因此在重排序后,如果线程B在代码3执行后,代码2执行之前执行了对象是否为空的检查语句,B线程就可能会看到一个还没有被初始化完成的对象。
可以有如下解决方案:
- 不允许2和3重排序
- 允许2和3重排序,但不允许其他线程“看到”这个重排序。
基于volatile的解决方案
只需要将instance声明为volatile变量即可。则代码2和3在多线程条件下将会被禁止重排序。
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
该方式虽然代码没有基于类初始化的方式实现的代码简洁,但是有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。
基于初始化类的解决方案
JVM 在类的初始化阶段(即在 Class 被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM 会去获取一个锁。这个锁可以同步多个线程对同一 个类的初始化。基于这个特性,实现方案如下:
public class Singleton {
private Singleton() {}
private static class InstanceHolder {
public static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.INSTANCE;
}
}
该方案实质是:允许伪代码中的 2 和 3 重排序,但不允许非构造线程“看到”这个重排序。
懒汉式-枚举方式
Effective Java这本书的作者推荐的方式。
public enum Singleton {
INSTANCE;
public void doSomething() {
// do something;
}
}
枚举的方式除了线程安全外,还有反射安全、序列化反序列化安全的优势。
反射安全?
即使对象的构造器是私有化的权限,仍然可以通过反射的方式获取该构造器,并通过反射获取的构造器实例化对象。
解决方式是:可以在构造函数在被第二次调用的时候抛出异常。
序列化安全?
是指将实例对象通过序列化再反序列化的方式操作一次后,序列化前后的对象会不相等。因为每次反序列化一个序列化实例的时候,都会创建一个新的实例。