Java设计模式 (1)——单例模式的五种写法

引言

为了更好的理解Spring框架里的IOC、AOP,最好先学习一些常见的设计模式(因为Spring源码里面设计模式满天飞),第一个就是大名鼎鼎的单例模式啦。

单例模式概念

单例模式简单来说就是一个类只有一个实例,单例模式遵循的原则如下:

  1. 整个类只能有一个实例,因此无法通过new()任意创建对象,构造函数为private。整个类维护的唯一实例在类的内部,且用static修饰。(若非static,还需在类外部先创建对象,再访问)
  2. 这个实例提供一个公共访问接口,通过这个接口可以访问到唯一的这个实例。这个访问方法也是静态的,理由同上。

单例模式的应用场景

单例模式可以解决的问题有以下几个:

  1. 有一些类的对象会被频繁创建和销毁,频繁的加载、初始化、销毁开销比较大,如果只需维护一个实例,就节省了那些开销。
  2. 对于“资源管理者”,单个实例便于管理资源、避免了同步问题。比如网站计数器、线程池、垃圾回收站、文件系统、日志。

五种单例模式

(一)饿汉式

饿汉式是最简单粗暴的单例模式,直接用一个静态域来存储整个类的唯一实例:

public class HungrySingleton{

    // 用静态域存储唯一实例
    private static final HungrySingleton instance = new HungrySingleton();

    // 私有构造方法
    private HungrySingleton(){
    }

    // 提供一个对外访问接口
    public final HungrySingleton getInstance(){
        return instance;
    }
}

之所以称之为饿汉式,是因为这个类还没等到人家访问他的实例,就急着在初始化的时候创建了实例。如果有很多单例类,每个类都这样的话,会非常浪费空间,因为他们的实例创建出来以后很可能并不急着用,白白占着内存。

因此,为了避免空间浪费,出现了懒汉式。instance在类加载、初始化的时候生成且生成后无法改变,所以不存在线程安全问题。代码中用final修饰了instance变量,如果需要释放instance资源的话则可以不加final。

(二)懒汉式

懒汉式相对饿汉式最大的区别就在于延迟加载,即一开始不急着创建实例,而是等到有人访问的时候再创建。最简单的懒汉式可以这么写:

public class LazySingleton {

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

上面的写法在单线程环境下没有任何问题,但是很显然在多线程环境下,如果多个线程同时判断出instance == null,会创建出多个实例,因此需要引入锁机制

最简单粗暴的做法就是在 getInstance() 方法前面加一个Synchronized修饰语,使其变为一个同步方法。但问题是,当instance不为null后,已经没有线程安全的隐患了,但多个线程想要同时访问instance仍然需要拿到锁,并发率太低,效率低下,因此需要把线程锁加到更小的范围内,比如下面这样:

public class LazySingleton {

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

要注意的是如果在synchronized修饰的代码块里不再次判断instance == null的话,刚才所提到的线程安全问题还是没有得到解决,因为在第一次冲突发生的时候,后面拿到锁的线程依旧会创建实例。

至此,懒汉式的线程安全问题得到解决,但是新的问题出现了:instance = new LazySingleton();这个语句不是一个原子操作,分为好几个步骤。下面做一个小实验来证明:
声明类A,在main函数里new一个实例并复制给变量a,代码如下:

public class A {
    public A(){}
    public static void main(String[] args) {
        A a = new A();
    }
}

接下来javap -v A.class, 反编译生成的class文件,得到如下的结果:
反编译的结果
可以看到 A a = new A()这个语句分为四个步骤:

  1. new指令:在java堆上为 A 类的对象分配内存空间,并将地址压入操作数栈顶;

  2. dup指令:复制操作数栈顶的值,并将其压入栈顶;

  3. invokespecial指令:调用实例初始化方法,弹出一个对象地址;

  4. astore_1 指令:从操作数栈顶取出 A对象的引用并存到局部变量表;

由于JVM会进行指令重排序(出于优化目的),所以这三个步骤的相对顺序不是固定的。因此需要考虑到的一种情况是:以上步骤发生的顺序是1->2->4->3,而在4与3之间另一个线程调用getInstance()方法,此时instance不为null,于是直接返回未初始化完毕的instance,便会引发问题。

为了解决JVM指令重排序的问题,一个很方便的解决方案是使用volatile修饰词,volatile也属于锁,可以禁止指令重排序,因此就出现了第三种单例模式——双重锁检验单例模式。

(三)双重锁检验机制

很简单,在刚才的基础上加一个volatile关键字来修饰instance就行。

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

(四)静态内部类懒汉式

懒汉式为了解决线程安全+指令重排序的问题,还有一个更简便的方法就是使用静态内部类,如下所示:

public class StaticClassSingleton {
    private static class CreateInstance{
        private static final StaticClassSingleton instance = new StaticClassSingleton();
    }

    private StaticClassSingleton(){
    }

    public static final StaticClassSingleton getInstance(){
        return CreateInstance.instance;
    }
}

静态内部类只有在被访问的时候才会进行初始化(详情可参考JVM虚拟机类初始化的时机),因此属于延迟加载。
这种方法之所以能在多线程环境下保证线程安全,是因为JVM保证类的初始化是线程安全的,因此instance只能被创建一次。

(五) 枚举式

public enum EnumSingleton{
    INSTANCE,
}

枚举类里面所枚举的对象都是单例的,这里的INSTANCE在JVM中会变成public static final EnumSingleton INSTANCE;。在上面的代码里只枚举了一个对象,而枚举类的构造函数是默认并强制私有的,无法在类外部创建对象,因此整个类只有INSTANCE一个对象。

访问时直接用EnumSingleton.INSTANCE即可。枚举类是实现单例模式最便捷的方式,同时还保证了安全(没有线程安全问题,阻止反序列化或反射创建新对象),但是没有延迟加载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值