常见的单例模式

单例模式

含义:确保一个类只有一个实例,并提供该实例的全局访问点

意义:实际开发中,一些特定的组件只需要一份即可,如果在需要该组件的时候现场去new这个组件,那么将会产生很多不必要的开销。单例模式很好地解决了这个问题,需要的时候创建或是提前创建好这个对象,在全局提供唯一的访问点,避免了很多该类对象大量创建和销毁的开销。

关键点:

  1. 构造器私有:保证类外部无法创建此类对象
  2. 一个私有的静态变量保存该类的唯一实例:私有保证该数据的安全,静态表示这个变量归类所有
  3. 一个公有的静态函数提供全局的唯一访问点:只能通过该函数返回唯一的私有静态变量

饿汉式-线程安全

1
2
3
4
5
6
7
8
9
10
11
package personal.test.singleton.ehan;

public class Singleton {
private static Singleton uniqueInstance = new Singleton();

private Singleton(){}

public static Singleton getUniqueInstance(){
return uniqueInstance;
}
}

java虚拟机保证了静态变量在内存中只有一个(静态变量位于方法区,方法区是线程共有的区域),所以Singleton是单例的:只有在该类被加载的时候才为静态变量分配内存,才有机会调用该类的构造器,而类只被虚拟机加载一次。

需要该类的实例化对象时,只需要将此变量引用的对象返回即可。

缺点:在类加载的时候就创建出了对象,但是该对象不一定立刻被需要,在真正需要之前一直占据着内存,造成资源浪费。假如该类的实例化对象一次也没被需要,那么创建它的过程也是种资源的浪费。

懒汉式-线程安全的写法

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton uniqueInstance;

private Singleton(){}

private static synchronized Singleton getUniqueInstance(){
if(uniqueInstance == null){
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}

这种方式只有在第一次需要的时候才被创建,故称懒汉。

延迟创建有诸多好处,不一一细举。

注意synchronized:

  • 除去这个关键字,便可能创建出多个对象(多个线程可能同时进入都判断的是uniqueInstance为null,都去创建实例化对象了,会多次调用构造的方法,这已经违背了初衷。 另外多线程环境下使用构造方法时,这种情况会出现this引用的溢出,其它线程会使用到一个线程未创建完成的对象,是十分不安全的)
  • 静态方法加上这个关键字,锁对象是类的类对象,意味着所有线程使用这个方法必须获取这个锁,所以这个函数里的代码块是串行的,保证了单例

缺点 :在创建对象的时候应该去加锁,保证只有一个线程创建对象。但是这种方式在已经创建对象完成之后就缺乏高效性,获取单例对象的过程是串行的,即一个线程获取对象必须等待其它线程获取对象的完成,无法并行获取。

双重校验锁-线程安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton uniqueInstance;

private Singleton(){}

public static Singleton getUniqueInstance() {
if(uniqueInstance == null){ //1
synchronized (Singleton.class){ //2
if(uniqueInstance == null){
uniqueInstance = new Singleton(); //3
}
}
}
return uniqueInstance;
}
}
  • 这种模式在获取单例对象的函数上没有加锁,而是在需要同步的代码块儿(创建对象的代码块)上加锁,既可以获取单例对象的效率,又保证了只有一个线程来创建对象。
  • 需要两次判断uniqueInstance是否为空,第一次判断是对象未创建时,由于是并行,可能有多个线程进入了1之内。虽然把他们放进来,但是不能让他们都创建对象,这时在2的地方加锁,一回只能让一个线程进去,如果2里面没有null值判断,那就进来一个创建一个对象,当第一个线程进去后出来,uniqueInstance已经不为null了,但是依然有其它线程在1内,它们还要进入2,进2不能让他们创建对象,判断不为null他们就直接出去。
  • 还有一个问题,就是之前静态变量没有加volatile,而这个加了此关键字,这是为了避免在并行环境下执行构造函数的this引用的溢出(volatile可以禁止指令重排),而上面的懒汉模式因为函数加了同步关键字,所以函数内部的代码是串行的。

this引用的溢出

new一个对象的步骤:

  1. 看class对象是否加载,如果没有就先加载class对象,

  2. 分配内存空间,初始化实例,

  3. 调用构造函数,

  4. 返回地址给引用

cpu有时会指令重排,将4放在3的前面,这在串行的模式下没有任何问题,毕竟先返回地址,把this引用赋值给变量,然后在变量使用前总会保证其构造完整的(构造先于使用)。

但是在并行的模式下就会发生一个线程使用了另一个线程未完全构造的对象,称之为this引用的溢出。

如上代码,3处只要把this赋值给uniqueInstance,它就不是null了,其它后进来的线程不进入1,直接返回uniqueInstance使用,uniqueInstance引用的对象可能还没有被创建单例对象的线程创建完整,这时就会不安全。

使用volatile可以禁止CPU指令重排,使创建对象的线程保证如上的1,2,3,4的步骤,这样当返回this引用的时候一定是对象创建完成的时候,可以放心使用。


常见的单例模式
https://blog.wangxk.cc/2021/03/24/常见的单例模式/
作者
Mike
发布于
2021年3月24日
许可协议