Home 单例模式(Singleton)
Post
Cancel

单例模式(Singleton)

天无二日,国无二主

定义

确保一个类只有一个实例,并提供一个全局的访问点。

使用场景

只需要有一个对象:线程池、数据库连接池、缓存、日志对象、处理偏好设置和注册表对象、任务管理器等

特点

  • 优点:
    • 节省内存空间
    • 避免频繁创建销毁对象,减轻GC工作,提高性能
    • 为整个系统提提供了一个全局访问点
  • 缺点:
    • 不适用于频繁变化的对象
    • 扩展困难

实现

饿汉式

1
2
3
4
5
6
7
8
9
10
public class HungrySingleton {
    private static HungrySingleton hungrySingleton = new HungrySingleton();

    public HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return hungrySingleton;
    }
}

线程安全

懒汉式(Double-check Locking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazySingleton {

    private static volatile LazySingleton lazySingleton = null;
    public LazySingleton() {
    }

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

保证线程安全的原理:
双重判空第一次判空,如果实例已存在,则直接返回实例,提升性能;第二次判空:当多个线程一起达到锁位置时,待第一个线程创建对象完成释放锁,其他线程竞争到锁后,判空直接返回实例
synchronized:保证需要执行创建对象的线程只能有一个
volatile可见性禁止指令重排序

  • 可见性:volatile修饰的变量的修改对其他任何线程都是可见的,两次判空依赖该特性
  • 禁止指令重排序:通过在创建对象的指令前后添加内存屏障来禁止指令冲排序

new关键字创建对象不是原子的,需要三步

1.在堆内存开辟内存空间
2.调用构造方法,初始化对象
3.引用变量指向内存空间

为了提高性能,JVM虚拟机常常会对既定的代码执行顺行进行指令重排序,经过指令重排序后,创建对象的顺序可能1、2、3,也有可能1、3、2。当执行顺序是1、3、2时,执行到3(引用变量指向内存空间)时,另一个线程执行if (lazySingleton == null)时,会拿到没有初始化的非NUll实例,从而出现著名的DCL失效问题。

静态内部类(Static-Inner)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StaticInnerSingleton {

    public StaticInnerSingleton() {
    }

    public static StaticInnerSingleton getInstance() {
        return InnerClass.INSTANCE;
    }

    private static class InnerClass {
        private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }
}

优点:懒加载、线程安全、效率较高、实现简单

静态内部类是怎么实现懒加载的?

一个类从被加载到虚拟机中开始,到卸载出内存为止,它的整个生命周期将经历加载验证准备解析初始化使用卸载七个阶段,其中验证、准备、解析统称为连接

类生命周期

对于类初始化阶段,《Java虚拟机规范》严格规定了有且只有六种情况必须立即对类进行初始化:

  1. 遇到newgetstaticputstaticinvokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
    • 使用new关键字实例化对象的时候
    • 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
    • 调用一个类型的静态方法的时候
  2. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化
  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
  5. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStaticREF_putStaticREF_invokeStaticREF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
  6. 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

getInstance()方法被调用时,InnerClass才在StaticInnerSingleton的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。

静态内部类是如何保证线程安全的?

Java虚拟机必须保证一个类的()方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行完毕()方法。如果在一个类的()方法中有耗时很长的操作,那就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

枚举(Enum)

1
2
3
4
5
6
7
public enum EnumSingleton {
    INSTANCE;

    public void exec() {
        System.out.println("exec...");
    }
}

优点:简单,高效,线程安全,可以避免通过反射破坏枚举单例

先编译再反编译EnumSingleton类:

1
2
javac EnumSingleton.java 
javap EnumSingleton 

结果:

1
2
3
4
5
6
7
8
Compiled from "EnumSingleton.java"
public final class com.neil.parent.EnumSingleton extends java.lang.Enum<com.neil.parent.EnumSingleton> {
  public static final com.neil.parent.EnumSingleton INSTANCE;
  public static com.neil.parent.EnumSingleton[] values();
  public static com.neil.parent.EnumSingleton valueOf(java.lang.String);
  public void exec();
  static {};
}

从枚举的反编译结果可以看到:

  1. INSTANCEstatic final修饰,所以可以通过类名直接调用
  2. 创建对象的实例是在静态代码块中创建的,因为static类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的,所以创建一个enum类型是线程安全的。

代码下载地址:https://github.com/ni-shiliu/neil-design-mode

参考:《Head First 设计模式》、《深入理解Java虚拟机》

This post is licensed under CC BY 4.0 by the author.
Trending Tags