当谈及单例模式时,我们指的是一种设计模式,它确保某个类只有一个实例,并提供一个全局访问点。在Java中,单例模式是最常用的设计模式之一,它可以确保一个类在应用程序的生命周期内只有一个实例,并提供全局访问点以便访问该实例。
单例模式主要用于以下几种情况:
在Java中,有多种方式可以实现单例模式,下面我们将介绍其中比较常见的三种实现方式:懒汉式、饿汉式和双重检查锁定。
饿汉模式是指在类加载时就创建实例,由于这个实例创建的非常早,所以使用饿汉描述,形容非常迫切。这种方式在多线程环境下也能保证单例的唯一性。
示例代码:
class Singleton1 {
private static Singleton1 instance = new Singleton1();
public static Singleton1 getInstance() {
return instance;
}
private Singleton1() {};
}
在上述代码中我们可以看到,我们在类属性中实例化了一个该类的对象,然后给出了一个获取这个对象的方法,然后我们把默认的无参构造方法实现为 “私有的”,让外界无法直接实例化新的对象。
懒汉式单例是指在需要时才创建实例。当第一次调用获取实例的方法时,才会创建实例对象。
示例代码:
class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
如果首次调用getInstance() ,此时instance为null,就会进入if条件,从而创建实例,等再次调用getInstance则不会再创建实例。
但是上如代码如果在多线程中调用,是有可能创建多个实例的:
假设有两个线程 t1, 和 t2 都在调用 getInstance ,当 t1 刚好执行到进入 if 内部 还没有 把创建好的实例赋值给 instance 的时候,被调度除了cpu,此时,instance 仍然等于 null, t2线程就有可能进入 if 内从而再次创建一个实例。
如何改进,让上述代码变为线程安全的代码?
上诉代码线程不安全的原因是,if 和 new 操作之间可能再次运行了其他线程的 if 和 new 操作,于是我们可以通过加锁,把这两个操作打包在一起:
class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {}
private static Object locker = new Object();
public static SingletonLazy getInstance() {
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
}
return instance;
}
}
上述代码就保证了 if 和 new 操作之间 不会执行其他线程的 if 和 new 操作。但是我们发现上述代码,其实只有在第一次调用的时候才需要加锁,后续都直接返回 instance 即可 ,但是每次仍然都是先加锁再解锁,这样会导致该代码的效率会变低,于是我们可以在该方法的上面再加一个 if 判断:
class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {}
private static Object locker = new Object();
public static SingletonLazy getInstance() {
if(instance == null) {
//如果 instance 为 null 说明是首次调用,需要加锁
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
注意上述代码中两个 if 的结果是可能不同的。第一个 if 和 第二个 if 之间可能 会执行其他 线程调用 的 getInstance 导致 instance 不为 null。
上述代码 虽然保证了 if 和 new 操作之间 不会执行其他线程的 if 和 new 操作,但仍可能存在线程安全问题。
指令重排序引起的线程安全问题:
指令重排序是指处理器在执行指令时可能会改变指令的顺序,以提高程序的运行效率。
我们来看这行代码:
instance = new SingletonLazy();
这行代码可以拆分为三个步骤(不是三个cpu指令):
正常情况下,上述步骤是按 1 2 3 的顺序来执行的,但是编译器也可能优化成 1 3 2来执行,
现在假设有 t1 , t2 两个线程 ,如果此时,t1 正在以 1 3 2,的顺序在执行这行代码,t1在刚执行了 1 和 3 之后 ,就被调度出了cpu ,此时,instance 的值已经不为空,但是指向的内存中却还没有创建 实例,此时 t2 线程又刚好开始执行 getInstance ,于是,t2 线程就得到了,一块没有被初始化的内存空间,此时如果,t2 再尝试使用这块内存空间中的内容,就会引发错误 此时的instance是一个“全0”的值。
所以我们 应该给 instance 加上 volatile 关键字, 来禁止指令重排序
class SingletonLazy {
private volatile static SingletonLazy instance = null;
private SingletonLazy() {}
private static Object locker = new Object();
public static SingletonLazy getInstance() {
if(instance == null) {
//如果 instance 为 null 说明是首次调用,需要加锁
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
现在上述代码就是一个线程安全的代码了
懒汉式单例模式: 优点:
缺点:
饿汉式单例模式: 优点:
缺点:
总结: 懒汉式单例模式具有延迟加载和线程安全的优点,但也存在一些线程安全问题和实现复杂的缺点。饿汉式单例模式简单直观,线程安全,但可能存在资源浪费和无法实现延迟加载的缺点。在选择单例模式实现方式时,需要根据具体需求和场景进行权衡和选择。