单例模式就是指在整个系统中,某个对象实例只存在一份,单例模式有很多应用场景,属于GoF23中设计模式中的创建型模式,借助单例模式可以解决系统的资源,单例模式看似简单,但是想要实现一个正确的单例模式却不并不是很简单。
从单例对象的加载时机来划分,单例模式有可以分为饿汉式和懒汉式,所谓的饿汉式就是指提前准备好单例对象,即使单例对象目前还没有使用,懒汉式就是指只有第一次用到单例对象时才会去创建单例对象。
单例模式提供了一个对象全局访问点,通过该访问点获取的单例对象可以保证是唯一的。

单例模式有很多实现方式,其中正确的有很多种,错误的也有很多种,这里先对正确的实现进行分析
饿汉式单例模式会在单例对象被使用之前就被创建。
首先类在加载的时候,会首先初始化静态成员,其次JVM在加载类的时候,会确保类只会被加载一次,因此静态成员只会被初始化一次。
package correct.impl1;
public class Singleton {
private final static Singleton SINGLETON = new Singleton();
public static Singleton getInstance() {
return SINGLETON;
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
将单例对象定义成一个枚举类,枚举类的成员天然就是单例的,并且还不可以被序列化,因此可以防止因为序列化产生多个对象,这也是Effective Java里面推荐的方式。
package correct.impl2;
public enum Singleton {
SINGLETON;
public static void main(String[] args) {
Singleton instance1 = Singleton.SINGLETON;
Singleton instance2 = SINGLETON.SINGLETON;
System.out.println(instance1 == instance2);
}
}
懒汉式指的是,只有枚举类对象第一次被使用时才会被创建,否则不会创建,相比饿汉式懒汉式显然更节约系统资源。懒汉式有很多种实现方法,而且极容易写出错误的实现。
加锁的方式对应的就是不加锁的错误实现方式,在错误案例里面会讲。加锁可以保证两个事情,一个是将获取单例对象的方法在多个线程之间互斥进行,并且锁的获取和释放可以保证A线程对instance变量的写入可以被B线程可见,因为有锁的出现,因此可以进行先检查在初始化。当A线程获取到锁的时候,会初始化instance,B线程获取到锁时,此时发现instance已经被初始化,因此直接返回A线程初始化的对象,可以做到全局单例,但是加锁会严重影响效率。
package correct.impl3;
public class Singleton {
private static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
4.2.1中给出的方法中,锁的范围太大,覆盖了整个方法,在线程竞争比较激烈的情况下,会严重影响程序的执行效率,因此4.2.2中采用给代码块加锁的方式,减小临界区的大小,加速锁的获取和释放速度。双重检查和volatile是为了保证单例的正确性,二者少其一,均不能正确实现单例模式(不是说会产生多个对象,而是在访问单例对象的时候可能出现错误),原因在错误案例中会分析。
package correct.impl4;
public class Singleton {
// 不能去除volatile
private volatile static Singleton instance;
public static Singleton getInstance() {
// 第一层判断只是初步过滤,如果去掉第一层,也没什么不妥
// 但是会导致效率下降,因为所有的线程还是一上来就尝试竞
// 锁
if (instance == null) {
// 在并发严重的情况下,大部分线程都会进入这个if
synchronized (Singleton.class) {
// 但是只有一个线程可以进入该代码块
// 其他线程阻塞,该线程判断是否为null
// 发现为null
if (instance == null) {
instance = new Singleton();
}
// instance被实例化完成
// 当初始化instance的线程退出代码块的时候
// 只有之前进入第一个if的那些线程,才会继续获取锁
// 但是此时instance一定不为null
// 因此不会参与实例化对象,会立马释放锁
// 后续大部分线程都会被阻挡在第一个if语句
// 因此可以提高效率
}
}
// 返回
return instance;
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
如果在一个类中定义一个静态内部类,那么在外部类加载的时候,并不会加载静态内部类,只有访问静态内部类或者其成员的时候才会导致静态内部类的加载,同时JVM会保证任何类的加载都只会执行一次,内部类也是如此,因此可以借助内部类方法实现单例的懒汉式。
package correct.impl5;
public class Singleton {
private static class InstanceHolder {
public static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
// 用户调用该静态方法会导致Singleton类的加载
// 而访问InstanceHolder的静态属性INSTANCE
// 又会导致INSTANCE被初始化(只被初始化一次)
return InstanceHolder.INSTANCE;
}
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance1 == instance2);
}
}
单例模式的模式的错误实现大多集中在懒汉式的实现方式上面,这里给出的是几种常见的懒汉式单例模式错误实现。
错误原因分析
package error.impl1;
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
下面这种方式,加锁基本上没有起到作用
只要同一时刻有多个线程进入if,锁就和没加一样。
package error.impl2;
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
这种方式的错误是最难发现的,因为他不会导致系统出现两个对象,但是会导致某个线程可能会访问到未初始化完毕的对象,这肯定是不行的,因为一个对象还没有初始化完毕,那么访问他的属性或者方法时就有可能出现问题,比如出现空指针异常等等。
这里要先说明一下,instance在多个线程之间不存在内存可见性问题,因为有锁的存在,线程在执行临界区代码的时候,会将对共享变量的修改直接写回内存,因此instance不会存在内存可见性问题,这里导致问题的是new Singleton()可能会发生重排序,没错,就是因为这个重排序导致某个线程可能访问到未初始化完全的对象,从而可能导致错误。
new一个对象,并不是一个原子的指令,它是由多条指令组合而成,这里假设new由三条指令组合,分别是
如果new严格按照123的顺序执行,不会有任何问题,关键是2和3可能发生重排序,假设发生了2和3的重排序即
然后此时有A线程和B线程
现在解释为什么要加volatile,因为volatile可以限制重排序,加了之后2和3就不会被重排序了
package error.impl3;
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}