单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例:
优点:
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
面试点:此方案可以考察应聘者对 synchronized 关键字的理解
getInstance()
方法没有使用 synchronized 关键字。的确,饿汉模式的线程安全不是依靠 synchronized 来保证的,而是依靠JVM。<clinit>
方法。<clinit> 方法专门负责执行 类变量初始化语句 和 静态初始化语句,而且<clinit>方法能够保证初始化类变量这一过程的线程安全。为此,我们说,<clinit>机制是饿汉模式能够实现线程安全的基础。public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
面试点:此方案可以考察应聘者对JVM中 类加载机制 和 对象加载机制 的理解。
public class Singleton {
// 这里一定要加上 volatile,为了禁止指令重排
private volatile static Singleton singleton;
private Singleton (){}
//双重检测锁模式
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
当 singleton = new Singleton();
创建实例对象时,并不是原子操作,它是分三步来完成的:
上述正常步骤按照1–>2–>3来执行的,但是,我们知道,JVM为了优化指令,提高程序运行效率,允许指令重排序。正是有了指令重排序的存在,那么就有可能按照1–>3–>2步骤来执行,这时候,当线程A执行步骤3完毕,在执行步骤2之前,被切换到线程B上,因为 singleton 指向了空间,所以 singleton 判断为非空,此时线程B直接来到return singleton
语句,拿走 singleton 然后使用,接着就顺理成章地报错(对象尚未初始化完成)。
volatile关键字其中一个作用就是禁止指令重排序,所以DCL单例必须要加volatile。
面试点:此方案可以考察应聘者对volatile关键字的理解和掌握。
<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()
方法完毕。如果在一个类的<clinit>()
方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()
方法后,其他线程唤醒之后不会再次进入<clinit>()
方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。public class Singleton {
// 构造器私有
private Singleton (){}
// 私有的静态内部类
private static class SingletonHolder {
// final 主要为了防止外部类实例对内部类参数进行修改,防止外部数据跟内部实例内的数据不一致
private static final Singleton INSTANCE = new Singleton();
}
// final 修饰的方法不能被重写
public static final Singleton getInstance() {
// 外部类可以访问内部类的 private 属性
// 非静态内部类中,不能有静态变量
// 静态内部类中的私有变量
// 如果是静态的,外部类直接通过 类名.变量名 即可访问
// 如果是非静态的,外部类直接通过 对象名.变量名 即可访问
return SingletonHolder.INSTANCE;
}
}
面试点:此方案可以考察应聘者对Java四种嵌套类的理解和掌握。
public enum Singleton {
INSTANCE;
// Singleton.INSTANCE.whateverMethod();
public void whateverMethod() {
}
}
面试点:此方案可以考察应聘者对枚举的理解和掌握,包括枚举的初始化、构造函数、values方法等。
因篇幅问题不能全部显示,请点此查看更多更全内容