面试常考题
请解释一下什么是单例模式,并给出一个使用场景。
单例模式有哪几种实现?如何保证线程安全?
单例模式有什么好处?写出单例模式代码。
单例模式是什么?
单例模式是一种设计模式,它确保一个类在整个运行过程中只有一个实例,并提供全局访问点来获取该实例。核心思想就是只实例化一次。
单例模式有哪几种实现方式?手写单例模式
单例模式的实现方式主要有饿汉式和懒汉式。
饿汉式就是指实例在类加载的时候就创建,线程安全。缺点就是不用的话浪费资源,启动速度慢。
懒汉式指实例在首次访问时创建(需要保证线程安全)。
饿汉式代码
public class Singnle{
private static final Singnle instance = new Singnle();
private Singnle(){};
public static Singnle getInstance(){
return instance;
}
}
饿汉式天然支持线程安全,当多个线程同时首次访问这个类时(如调用getInstance()
),JVM会确保只有一个线程能执行类的初始化(其他线程阻塞等待)。
懒汉式代码
public class Singnle{
private static Singnle instance;
private Singnle(){}
public static Singnle getInstance(){
if(instance==null){
instance = new Singnle();
}
return instance;
}
}
但是应该考虑线程安全问题,这样写不是线程安全的,万一多线程同时进来就会创建多个实例。我们应在返回实例的时候加锁。然后就有了下面的代码:
//其他代码省略,这里只改动get方法
public static synchronized Singnle getInstance(){
if(instance == null){
instance = new Singnle();
}
return instance;
}
看完上面的代码你可能不会觉得有什么问题,如果要使用这个实例的话怎么办?一般是在main方法里面调用这个静态方法,比如:
public class Main {
public static void main(String[] args) {
// 获取单例对象
Singleton singleton = Singleton.getInstance();
}
}
这里就会产生一个问题,假设我的单例模式实现的是一个日志管理器,那我在每个类中每次使用这个实例对象的话都要调用这个getInstance()方法,每次调用这个方法都会加锁,但是加锁的初衷是创建这个实例的时候防止线程竞争,显然在方法上加锁就非常影响性能了。所以我们应该在第一次创建实例的时候加锁,修改代码如下:
public class Singnle{
private static volatile Singnle instance;
private Singnle(){}
public static Singnle getInstance(){
if(instance == null){
synchronized(Singnle.class){
if(instance == null){
instance == new Singnle();
}
}
}
return instance;
}
}
注意到上面这段代码,修改的部分有:instance前面加了一个volatile修饰,之前的一个if现在变为了两个if。首先我们先讲一下为什么要加volatile,volatile是java中的一个关键字,它在此处的作用是保证这个共享变量的可见性和有序性。可见性就是该变量被一个线程修改之后能立刻被其他线程感知(立刻刷新到主内存)。还有一个作用就是禁止指令的重排序。学过JVM的我们知道,创建对象的过程并不是原子性的,它其实可以被划为三步操作:
1,在堆中分配内存空间
2,初始化对象实例(实例化)
3,在虚拟机栈的栈帧中的局部变量表中的对象引用(reference)赋值(该值就是在堆中的内存地址)
了解了对象创建的过程我们就能理解了这里为什么要用volatile,如果没加volatile的话,cpu会进行重排序(最大程度的提高性能但不会影响结果),但是如果这里进行重排序的话,比如说顺序改为了1-》3-》2,那么同时呢在别的线程访问这个变量的时候,比如调用instance的其他方法,由于这个指令重排序了,这个instance就不是null了,但是还没进行完对象的实例化,就会导致这个对象还没完全准备好呢就访问它,从而导致错误。
还有一个问题,为什么这里使用了双重检查锁定呢?我们可以详细的一步一步地推导多线程的执行步骤
假设有很多线程使用Singnle.getInstance(),然后多个线程就会同时进入这个方法。由于此时实例还没有创建,所有在红色箭头处,所有的线程判断结果都是一样的,都会进入这个if,然后都到了这个绿色箭头处。再往下执行的话就有synchronized关键字了,所以只会有一个线程进入,去创建这个实例。其他线程卡在绿色箭头处。然后实际工作的线程在执行完new Singnle()的代码后,就会解锁然后return。这里就会出现问题,之前跟它一起竞争的线程还卡在synchronized之外呢,不管他是cas自旋也好,阻塞也好,那个执行创建实例的线程解锁后,其他线程会重新竞争进入锁,!!!,此时如果没有这个第二次判断,那么,其他的线程就会又一次的执行这个创建实例的代码,显然就违背单例模式了。