Skip to content

单例模式

zhangpan edited this page Sep 6, 2021 · 1 revision

1.双重校验锁实现单例

public class DoubleCheckSingleton {

    private volatile static DoubleCheckSingleton singleton;

    private DoubleCheckSingleton() {
    }

    public static DoubleCheckSingleton getInstance() {
        if (singleton == null) {
            synchronized (DoubleCheckSingleton.class) {
                if (singleton == null) {
                    singleton = new DoubleCheckSingleton();
                }
            }
        }
        return singleton;
    }
}

(1)为什么要进行双重判空?

  • 第一次判空 在同步代码块外部进行判断,在单例已经创建的情况下,避免进入同步代码块,提升效率
  • 第二次判空 为了避免创建多个单例。假设线程1首先通过第一次判空,还未获得锁时时间片就用完了,此时,线程2获得CPU时间片,并调用单单例方法,此时singleton仍然为空,于是线程2顺利创建了singleton对象。稍后,线程1获得时间片,由于已经执行过了第一层判空,此时如果没有第二次判空,那么线程1也会再创建一个singleton实例,即不满足单例的要求。所以第二此判空很有必要。

(2)成员变量singleton为什么要使用volatile修饰?

编译器为了优化程序性能,可能会在编译时对字节码指令进行重排序。重排序后的指令在单线程中运行时没有问题的,但是如果在多线程中,重排序后的代码则可能会出现问题。

双重锁校验中实例化singleton的代码在编译成字节指令后并不是一个原子操作,而是会分为三个指令:

  • 1.分配对象内存:memory = allocate();
  • 2.初始化对象:instance(memory);
  • 3.instance指向刚分配的内存地址:instance = memory;

但是由于编译器指令重排序,上述指令可能会出现以下顺序:

  • 1.分配对象内存:memory = allocate();
  • 2.instance指向刚分配的内存地址:instance = memory;
  • 3.初始化对象:instance(memory);

假设这个singleton的实例化经过了编译器的重排序,此时有一个线程1调用了该单例方法,并在执行完上述第2步后用完了CPU事件片,此时singleton对象实际上还没有被初始化,但是singleton却被赋值指向了第一步分配的内存。此时,一个线程2获得CPU时间片,并调用这个单例方法,发现singleton不为null,随即return了singleton,但singleton实际上并没有初始化,因此可能造成程执行序异常。

如果给成员变量加了volatile关键字,那么编译器便不会对其进行指令重排序,也就不会出现上边的问题。

(3)双重校验锁优缺点

既能保证线程安全,又能实现延迟加载。缺点时使用synchronized关键字会影响性能。

2.静态内部类实现单例

public class StaticSingleton {
    private StaticSingleton singleton;

    private StaticSingleton() {
    }

    private static class SingletonHolder {
        public static StaticSingleton INSTANCE = new StaticSingleton();
    }

    public static StaticSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

这种单例利用了类加载的特性,在《Java虚拟机规范》中对于类初始化的时机有着严格的约束:

① 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

② 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

③ 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

④ 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

⑤ 当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。

⑥ 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

从上述类初始化的条件可以看出,如果不调用SingletonHolder.INSTANCE,SingletonHolder类就不会被加载到虚拟机,SingletonHolder不被加载到虚拟机那么INSTANCE实例也不会被实例化。

而当调用了getInstance后会调用SingletonHolder.INSTANCE,这里是一个getstatic指令,所有会触发SingleHolder的初始化,初始化前会进行SingletonHolder的类加载,在类加载的初始化过程中INSTANCE会被实例化。

3.枚举实现单例

// 枚举单例
public enum EnumSingleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("通过枚举单利打印日志...");
    }

}

// 测试类
public class Test {
    public static void main(String[] args) {
        EnumSingleton.INSTANCE.doSomething();
    }
}

枚举实现的单利是最完美的一种方式。这种方式可以防止序列化与反序列化造成创建多个实例的问题,而前面的几种方式都无法解决这个问题。

公众号:玩转安卓Dev

Java基础

面向对象与Java基础知识

Java集合框架

JVM

多线程与并发

设计模式

Kotlin

Android

Android基础知识

Android消息机制

Framework

View事件分发机制

Android屏幕刷新机制

View的绘制流程

Activity启动

性能优化

Jetpack&系统View

第三方框架实现原理

计算机网络

算法

其它

Clone this wiki locally