1.单例模式
1.1 什么是单例模式
单例模式是一种设计模式,用于确保一个类只能创建一个实例,并提供全局访问该实例的方法。
单例模式的核心思想是将类的实例化操作封装在类的内部,并提供一个静态方法或静态变量来获取该实例。这个静态方法或变量可以确保在整个应用程序中只存在一个实例。
单例模式有以下几个优点:
- 节省内存,避免频繁创建和销毁对象。
- 避免对共享资源的多重占用。
- 可以全局控制。
1.2 单例模式(饿汉式)
public class Singleton {
//static 关键字 保证只有一个实例
private static Singleton singleton = new Singleton();
//无法通过 new 的方式来创建实例
private Singleton(){}
//只能通过该方法获得实例
public static Singleton getInstance(){
return singleton;
}
}
class Main{
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
结果:true
之所以叫“饿汉式”,是因为它在类加载时就立即创建实例。与字面上的意思一样,它可以理解为“一开始就饿着肚子(渴望被使用)”。
优点:实现简单直观,线程安全。由于实例在类加载时就被创建,所以不存在多线程并发访问创建实例的问题,不需要考虑线程同步。
缺点:类加载时就创建实例,无论是否使用,都会占用一定的内存空间。
1.3 单例模式(懒汉式)
class Singleton2 {
private static Singleton2 singleton2 = null;
private Singleton2(){};
public static Singleton2 getInstance() {
if(singleton2 == null){
singleton2 = new Singleton2();
}
return singleton2;
}
}
懒汉式单例模式得名于它的延迟加载特性。与字面上的意思一样,懒汉式可以理解为“比较懒,需要的时候再去获取”。
优点:实现了延迟加载,即在第一次使用时才创建实例,避免了不必要的资源占用。
缺点:在多线程环境下,懒汉式单例模式需要考虑线程安全的问题。
上面的代码存在一个问题:可能有多个线程同时进入 if
条件判断,这时候还没来得及创建第一个实例,从而使得这几个线程都进入了if
语句里,最终导致前前后后创建了多次实例。虽然它们共享的一直是相同的实例(static关键字,多个线程同时进入 getInstance()
方法并创建多个实例,最终只会存在一个实例。),但是单例模式的目标是保证整个程序中只有一个实例存在,并且任何时候都只能获取到这一个实例。
1.4 单例模式(线程安全的懒汉式)
修改后(版本一):
class Singleton2 {
private static Singleton2 singleton2 = null;
private Singleton2(){};
public static Singleton2 getInstance() {
//加锁
synchronized(Singleton2.class){
if(singleton2 == null){
singleton2 = new Singleton2();
}
}
return singleton2;
}
}
上面的版本还是有一点问题,这里的加锁只是在new
出来之前加上是有必要的。但是new
完后,后续调用 get
就没有必要锁竞争了,因为singleton2
一定是非空的。
版本二:
class Singleton2 {
private static Singleton2 singleton2 = null;
private Singleton2(){};
public static Singleton2 getInstance() {
if(singleton2 == null){
//加锁:
synchronized(Singleton2.class){
if(singleton2 == null){
singleton2 = new Singleton2();
}
}
}
return singleton2;
}
}
这里的前后两个if
的含义是不同的,前一个if
用来判断是否要加锁,后一个if
用来判断是否创建实例。
那么这里的代码是否是正确的呢?还有问题!这里涉及到内存可见性问题和指令重排序问题。用volatile
关键字可以解决这两个问题。
最终版:
class Singleton2 {
private static volatile Singleton2 singleton2 = null;
private Singleton2(){};
public static Singleton2 getInstance() {
if(singleton2 == null){
//加锁:
synchronized(Singleton2.class){
if(singleton2 == null){
singleton2 = new Singleton2();
}
}
}
return singleton2;
}
}
这里对公共变量singleton2
同时进行读、写操作,所以涉及到了内存可见性问题。对于什么是内存可见性问题,可以看我往期的文章:(Java中的线程安全 与 synchronized、volatile关键字 - 掘金 (juejin.cn))
什么是指令从排序呢?指令重排序是指处理器为了提高程序性能,在不改变语义的情况下对指令进行重新排序。这可能导致某个线程在访问到 singleton2
时,它的引用不为 null
,但实际上实例还未完成初始化。
具体来说,new
这个操作不是原子的,它大致可以拆分成三个步骤:
- 申请内存空间。
- 调用构造方法,把这个内存空间初始化成一个合理的对象。
- 把内存空间的地址赋值给变量。
假如有两个线程t1
、t2
。t1
是按照1 3 2
顺序的步骤执行的。
假如t1
执行完“分配地址”这一步骤后,被切出CPU
让t2
来执行。t2
执行到最外层的if
时候,发现此时的singleton2
非空,最后直接返回了一个实例,而这个实例还没有被初始化,t2
可能会尝试去引用这个实例中的属性。这就会对程序造成未知的影响。