单例模式

单例模式应该是大家最为熟知的一种设计模式了,相信大家或多或少的都在自己的项目中使用过单例模式,例如封装一个Log工具类、一个数据库存取类或者用户登录管理类等。而我们使用单例模式主要有两个目的:

  1. 减少内存消耗
  2. 保证某些共享资源的唯一性

单例模式的写法有好多种,如:饿汉式单例模式、懒汉式单例模式、IoDH单例模式、枚举式单例模式,下面就来一一实现下这些写法。

饿汉式单例模式

1
2
3
4
5
6
7
8
9
10
11
public class HungrySingletonV1 {

private static HungrySingletonV1 instance = new HungrySingletonV1();

private HungrySingletonV1(){
}

public static HungrySingletonV1 getInstance(){
return instance;
}
}

上面的例子中,类被加载时,静态变量instance就会被初始化,这时候单例类的唯一实例就被创建了。

  • 优点:类加载时就已经实例化,避免了线程问题
  • 缺点:由于类加载的时候就实例化了,没有达到懒加载的效果,可能会造成内存浪费

饿汉式单例模式还要一种变种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HungrySingletonV2 {

private static HungrySingletonV2 instance;

static {
instance = new HungrySingletonV2();
}

private HungrySingletonV2(){
}

public static HungrySingletonV2 getInstance(){
return instance;
}
}

效果与前面的一样,不过把初始化方法放到静态代码块中,也是在类加载时调用。

懒汉式单例模式

低配版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LazySingletonV1 {

private static LazySingletonV1 instance ;

private LazySingletonV1(){
}

public static LazySingletonV1 getInstance(){
if(instance == null){
instance = new LazySingletonV1();
}
return instance;
}
}

上面的代码是最简单的懒汉式单例模式,实现懒加载,每次获取实例时会去判断是否已创建实例,如果一直没人用,则不用创建实例,节省内存空间。

但实际上,上面这种写法应该是最不推荐的一种单例模式写法。因为它是线程不安全的,如果多个线程同时获取该实例,就会创建多个实例对象,不符合单例的需求。

进阶版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LazySingletonV2 {

private static LazySingletonV2 instance ;

private LazySingletonV2(){
}

public static synchronized LazySingletonV2 getInstance(){
if(instance == null){
instance = new LazySingletonV2();
}
return instance;
}
}

可以看到在这种懒汉式单例模式中,我们在获取实例的方法上加了一个同步锁,这样保证了获取实例的方法在不同线程中是同步的,使得获取的实例是唯一的。

但是这种写法有一个最大的问题就是,效率太低。每一个线程都要进行等待,而实际中,如果已经创建了,后面的想获取实例,直接返回就行。

再进阶版

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

private LazySingletonV3() {

}

public static LazySingletonV3 getInstance() {
if (instance == null) {
synchronized (LazySingletonV3.class) {
instance = new LazySingletonV3();
}
}
return instance;
}
}

基于前一个版本的问题,有人想到了将同步锁置于instance为空判断之后,这样就实现了当实例已经创建,后面的获取时直接返回的问题。

但是,这种写法又导致了线程不安全。如果A用户拿到了同步锁,正在创建实例,另一个B用户在实例还未创建时到了同步锁外等候,当A用户创建完实例,退出同步锁后,B用户马上就获取了同步锁并开始创建实例,这就导致了创建了多个实例。

终极版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//双重校验锁
public class LazySingletonV4 {
private volatile static LazySingletonV4 instance;

private LazySingletonV4(){

}

public static LazySingletonV4 getInstance(){
//检查实例是否存在,不存在才进入同步块
if(instance == null){
//同步块,保证线程安全
synchronized(LazySingletonV4.class){
//再次检查实例是否存在,不存在才创建实例
if(instance == null){
instance = new LazySingletonV4();
}
}
}
return instance;
}
}

双重校验算是懒汉式单例模式的终极版本了,先判断实例是否为空,为空获取同步锁,在同步锁内再判断一次实例是否为空,既保证了线程安全又提高了效率。

双重校验加锁的实现一般会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

IoDH单例模式

什么是IoDH呢?

IoDH是Initialization Demand Holder 的缩写,简单来说就是在单例类中增加一个静态内部类,在该内部类中创建单例类的实例。

我们知道,在多线程开发时,为了解决并发问题,我们会使用synchronized来加互斥锁进行同步控制。但是在某些情况下,JVM已经隐含为您执行了同步,这些时候就不需要自己进行同步控制了。这些情况包括:

  1. 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
  2. 访问final字段时
  3. 在创建线程之前创建对象时
  4. 线程可以看见它将要处理的对象时

例如饿汉式单例模式,就是在类加载时进行了初始化,也就是由静态初始化器初始化的。但是饿汉式单例模式不符合懒加载的要求,如果可以让类加载时不去初始化对象,不就解决问题了吗。这就是IoDH的方法,通过定义一个静态内部类,在这个静态内部类中创建单例类实例,当我们需要使用时才会去加载这个静态内部类,创建单例类实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class IoDHSingleton {

private IoDHSingleton() {

}

private static class SingletonHolder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static IoDHSingleton instance = new IoDHSingleton();
}

public static IoDHSingleton getInstance() {
return SingletonHolder.instance;
}
}

当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建IoDHSingleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

枚举式单例模式

借助JDK1.5中添加的枚举来实现单例模式,应该是最好的实现单例模式的方式,代码也很简单。

1
2
3
4
5
6
7
8
9
public enum EnumSingleton {

instance;


public void method(){
//功能方法
}
}

访问也很简单,通过EnumSingleton.instance即可调用枚举类中的方法了。这种单例模式不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

总结

在实际项目中 饿汉式单例模式、懒汉式单例模式终极版、IoDH单例模式、枚举式单例模式 都是可以选择的实现方式,看个人喜好。不过枚举式单例模式应该是最简单且安全的实现方式,推荐使用。


源码:https://github.com/lichenming0516/DesignPattern

坚持原创技术分享,您的支持将鼓励我继续创作!