设计模式系列(单例)

设计的目的与使用场景

单例模式的设计目的是保持某些类在整个JVM中只有唯一的一个实例,我们在项目的任何地方拿到该对象的实例都应该是一样的(引用的是同一个对象)。
这种设计模式的使用场景一般都比较固定,一般如项目的配置信息、工具类等。
上面简要说了一下我们要达到的目的以及使用场景,代码的实现方式有很多种,下面会一一列举,这里先说一下实现这种设计模式需要遵循的几点要求:

  • 全局唯一(在使用场景下这个是必须要达标的,否则就不叫单例了)
  • 懒加载
  • 性能优异

其实能达到上述要求的实现方式是很有限的,但我们在网上查询单例的实现方式的时候回见到很多种版本,其中很多版本都是有缺陷的,但为什么还是会有很多人拿出来讲呢?
应该是为了列举不同实现方式的优缺点,方便我们理解与改进。所以有时候有缺陷的东西我们不一定会弃用,而是会拿来作为反面例子帮助我们理解。

实现方式的几大阵营

恶汉式

使用静态常量(可以少量使用)

代码示例:

public class Hungry {

    private Hungry(){}

    private static final Hungry hungry = new Hungry();

    public static Hungry getInstance(){
        return hungry;
    }
}

优点:
恶汉式应该是最简单粗暴的,因为这种实现方式使用的技术是Java的静态常量,因为Java中的静态常量在类被加载到JVM中时就会被初始化,这样该类只会被初始化一次,而且因为是常量后续也不会被修改。

缺点:
这种实现方式的唯一缺点就是没有实现懒加载,在项目初始化的时候就将这些单例加载到内存中,如果项目中这类单例内存占用率不是很高的话也还可以接受。
对于服务器应用来说一般不会特别在意应用的启动时间,但在做客户端的时候就需要注意这种单例的实现方式了,有可能我们初始化做了很多耗时的操作造成应用启动时间过长。

懒汉式

懒汉式与恶汉式很明显的区别就是懒汉式中的单例都是在使用的时候进行加载的,所以懒汉式的都是能达到懒加载目的的。

懒汉式一(不适合多线程)

代码示例:

public class LazyOne {

    private LazyOne(){}

    private static LazyOne instance = null;

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

优点:
代码实现方式上与恶汉式很相似,只是将实例的初始化放在了静态方法getInstance()中,不会在程序启动的时候就初始化。

缺点:
缺点也很明显,如果是多线程来调用的话会存在多个实例。

懒汉式二(不适合高并发)

代码示例:

public class LazyTwo {

    private LazyTwo(){}

    private static LazyTwo instance = null;

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

优点:
这种方式与懒汉式一的实现方式很相似,为了解决线程不安全的问题,在方法上加了synchornized关键字。

缺点:
在方法上面加上synchornized关键字会严重影响程序的性能,因为程序每次获取示例的时候都需要获得锁才能拿到实例,其实线程不安全的情况只会出现在实例初始化的那段期间。

懒汉式三(不适用多线程)

代码示例:

public class LazyThree {

    private LazyThree() {
    }

    private static LazyThree instance = null;

    public static LazyThree getInstance() {
        if (instance == null) {//1
            synchronized (LazyThree.class) {//2
                instance = new LazyThree();//3
            }
        }
        return instance;
    }
}

优点:
不会每次获取实例时都要获取锁

缺点:
lazyOne在多线程情况下会出现多个实例,假设线程A获取锁并执行到了注释3处,但是layOne还没有初始化,或者lazyOne初始化了但是没有同步到主存区则线程B还是会进入到注释2处,这样线程B在下一轮调度中获取到锁就可以再实例化lazyOn。

懒汉式四(适用高并发) 【双重检查 + 锁 + volatile】

代码示例:

public class LazyFour {

    private LazyFour() {
    }

    private static volatile LazyFour instance = null;

    public static LazyFour getInstance() {
        if (instance == null) {//1
            synchronized (LazyThree.class) {//2
                if (instance == null) {//3
                    instance = new LazyFour();//4
                }
            }
        }
        return instance;
    }
}

这种写法是目前使用的最多的,不仅加上了双重检查锁校验,而且还加上了volatile关键字。
首先volatile是因为内存可见性的原因,主要是为了同步线程工作区内存与主存中的数据。
注释1处的空值检查是因为性能的原因,上面已经说过了,注释3处的空值检查是为了防止某些阻塞在注释2处的线程在获取锁之后也会立即去创建新的实例(如果上个获取锁的线程已经创建实例了)。

优点:
满足开头讲述的几个条件,也是应用中经常使用的实现方式。

缺点:
除了会多一些代码,没什么缺点。

注册式

TODO:待完善

枚举式

TODO:待完善

发表评论

电子邮件地址不会被公开。 必填项已用*标注