写在前面:最近学了很多设计模式,好多没有学精,这里针对面试中经常出现的问题,往深挖掘。
定义:主要是确保一个类只能有一个实例,并且提供该实例的全局访问
基本方法:使用私有构造函数,为了不能通过构造函数来创建对象的实例,只能通过公有的静态函数返回唯一的私有静态变量。
说明:在面试过程中,面试官一定会问道线程安全的问题,这里需要对这两种的模式的优点与缺点,做进一步的说明与研究,争取征服面试官。
懒汉式就像懒汉一样只有需要时才会分配线程。,下面是懒汉式的实现方式
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
分析:1.首先创建静态成员变量 2.私有化构造方法,防止外部进行实例化 3.定义全局静态函数
在懒汉式里面的私有静态变量 uniqueInstance 被延迟实例化了,其中的好处是,如果没有用到这个类,那么它就不会被实例化,进而节省了资源。
在这里假如多个线程同时进入判断if(uniqueInstance==null),的时候,并且如果同一时间静态实例均为空的时候那么就会有多个线程进行实例化,那么也会导致多次实例化uniqueInstance。
饿汉式顾名思义就像饿汉一样,直接进行实例化变量,如下代码
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {
}
public static Singleton getUniqueInstance() {
return uniqueInstance;
}
}
方式:1.直接在私有化静态成员变量并同时创建对象实例,在调用该类实例化时候只有一个实例对象。
这里的线程是安全的,因为在初始化静态成员变量的时候就已经实例化了,这里即使多线程同时访问时候,也不会再进行实例化了。因此getUniqueInstance不会实例化多次,那么也就不会发生线程不安全的问题。
这里的缺点也很明显,丢失了延迟实例化所带来的好处,并没有节约资源。
面试中也会被问到,如何让线程变得安全呀!这时要是说不会就凉了,这里我整理了一份答案,后续能用得上。
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {
}
public static synchronized Singleton getUniqueInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
解决方法:这里只需要对公有静态成员方法加锁,getUniqueInstance() 方法加锁,加synchronized的锁
这里当一个一个线程在进入这个方法以后,那么其他的线程就必须等待,即使静态成员变量已经被实例化了。
但是缺点也很明显:这种方法会让线程变得阻塞,并且该方法有性能上的问题,因此不推荐使用。
分析:对于线程的安全问题,对于上一步所说的问题,要静态实例被实例化一次就可以直接使用了,那么对于加锁的操作只需要对实例化的一部分代码进行实例化,即只有当线程没有被实例化的时候,才需要进行加锁。
对于双重校验锁需要先判断是否被实例化,如果没有被实例化那么对其进行加锁。
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//判断是否被实例化
if (uniqueInstance == null) {
//对没有实例化加锁
synchronized (Singleton.class) {
//判断是否进行实例化
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
对于这种方法肯动会有小伙伴问了,你这不是多此一举了吗?为啥用两个if 而不是用一个if那?
对此小鼠同学进行解答:
如下代码就是你的想法
if (uniqueInstance == null) {
synchronized (Singleton.class) {
uniqueInstance = new Singleton();
}
}
这里只有一个if语句,当两个线程同时访问的时候加入为null,那么两个线程都会进入到代码块当中,即使有锁的操作,但是着两个线程依然会创建两个实例,那么就又变成线程不安全了。
当有两个if的操作时,即使两个线程进去了,其中以一个线程实例化操作以后那么另外一个线程判断if (uniqueInstance == null)时候发现已经被实例化了那么也就没办法在进行实例化了。
总结:第一个if 防止锁的进入,通过判断是否实话化了,也就没必要加锁了,对于第二个if防止创建多个实例。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();
这段代码其实是分为三步执行:
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
当 Singleton 类被加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance()
方法从而触发 SingletonHolder.INSTANCE
时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。
这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。
public class Singleton {
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getUniqueInstance() {
return SingletonHolder.INSTANCE;
}
}
public enum Singleton {
INSTANCE;
private String objName;
public String getObjName() {
return objName;
}
public void setObjName(String objName) {
this.objName = objName;
}
public static void main(String[] args) {
// 单例测试
Singleton firstSingleton = Singleton.INSTANCE;
firstSingleton.setObjName("firstName");
System.out.println(firstSingleton.getObjName());
Singleton secondSingleton = Singleton.INSTANCE;
secondSingleton.setObjName("secondName");
System.out.println(firstSingleton.getObjName());
System.out.println(secondSingleton.getObjName());
// 反射获取实例测试
try {
Singleton[] enumConstants = Singleton.class.getEnumConstants();
for (Singleton enumConstant : enumConstants) {
System.out.println(enumConstant.getObjName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
该实现可以防止反射攻击。在其它实现中,通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象,如果要防止这种攻击,需要在构造函数中添加防止多次实例化的代码。该实现是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击。该实现在多次序列化和序列化之后,不会得到多个实例。而其它实现需要使用 transient 修饰所有字段,并且实现序列化和反序列化的方法。