目录

设计模式

单例模式

在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线程就可能会看到一个还没有被初始化完成的对象。

可以有如下解决方案:

  1. 不允许2和3重排序
  2. 允许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;
  }
}

枚举的方式除了线程安全外,还有反射安全、序列化反序列化安全的优势。

反射安全?

即使对象的构造器是私有化的权限,仍然可以通过反射的方式获取该构造器,并通过反射获取的构造器实例化对象。

解决方式是:可以在构造函数在被第二次调用的时候抛出异常。

序列化安全?

是指将实例对象通过序列化再反序列化的方式操作一次后,序列化前后的对象会不相等。因为每次反序列化一个序列化实例的时候,都会创建一个新的实例。