单例模式

单例模式是最简单、同时也是使用最为广泛的设计模式之一。这种模式保证一个类只能拥有一个实例,并提供一个全局的访问点。值得注意的是,单例模式的实现必须满足三个必要条件,缺一不可:

  • 单例类的构造函数必须使用private修饰。这可以保证该类无法从外部创建实例。
  • 单例类中通过一个静态私有变量来存储唯一实例。
  • 单例类中必须提供一个公开的静态方法,供外部使用者访问唯一实例。

单例模式的实现也有多种方式,其中最为人知的为饿汉式和懒汉式。

饿汉式

饿汉模式的典型实现代码如下:

public class HungrySingleton {
    // 要素一:通过一个静态私有变量来存储该类的唯一实例。
    private static final HungrySingleton instance = new HungrySingleton();
    // 要素二:私有构造方法保证无法从外部创建该类的实例。
    private HungrySingleton() {}
    // 要素三:提供一个公开的静态方法,供外部使用者访问。
    pulbic static HungrySingleton getInstance() {
        return instance;
    }
}

饿汉式如同它的名字一般,在类加载时就如同“饿汉”一样,创建该类的实例,并保存在静态私有变量中。由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。饿汉式天生就是线程安全的,所以我们无需考虑多线程环境下的线程安全问题。

懒汉式

饿汉模式好像很不错,既能满足单例模式的要求,而且也无需考虑线程安全问题,那为什么还有懒汉式呢?饿汉模式决定了即使后续没有用到,也会在类加载时创建实例。因此,会拖慢应用启动速度,浪费内存资源。

我想:这问题简单,那把创建实例的时机延缓到第一次使用时不就行了吗?于是写出了如下代码:

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

上面的代码看似没问题,但有经验的开发者一眼就能看出,其实这段代码是线程不安全的,在多线程环境下可能会创建多个实例。知道了问题所在,解决起来也非常方便,我们可以使用Java提供的synchronized关键字1包裹起来。于是,改进后的代码如下:

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

[1]synchronized:这是Java中使用的同步锁。当锁住的代码块内容执行完成或抛出异常时才会自动释放锁。而没有拿到锁的线程无法进入代码块执行,需要等待。该关键字可以标注在方法上、类上、或单独锁住一个代码块中的代码,这里只对标注在方法上的情况进行说明,其他的不再赘述。

  • 若该关键字标注在普通方法上,例如:

    public synchronized void test() {...}
    

    实际上等价于:

    public void test() {
        synchronized (this) {
            ...
        }
    }
    

    也就是锁住了该类的对象。

  • 若该关键字标注在静态方法上,例如:

    public synchronized static void test(){...}
    

    此时就不一样了,我们知道静态方法是属于类的,而不是某一个类的实例。因此这里加锁的对象就变成了当前类。上述代码块等价于:

    // 当前静态方法在xxx类中
    public static void test() {
        synchronized (xxx.class) {
            ...
        }
    }
    

改进后的代码满足了延迟加载的需求,也保证了线程安全,可以在多线程环境中使用。

双检锁

懒汉式已经很完美了,但是仍然有可以改进的地方。我们不难发现,加的同步锁似乎只会在实例创建时用到,而一旦实例创建完毕,也就不存在线程安全问题了。而后续其他使用者每次获取该类的实例,都会经过同步操作,加锁释放锁的操作是很重的,会显著影响性能,这把锁的存在成为了一个累赘。

为了解决这个问题,我们可以在原来的代码外面再包裹一层if判断,形成两层检查,比如这样:

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

这样,当实例创建完成以后,外部的if判断就不通过了,因此不会执行同步代码块中的内容,规避了频繁加锁释放锁的性能问题。

同时我们似乎还发现了一个与之前的实现不一样的地方:静态属性上被添加了volatile关键字。该关键字涉及到指令重排问题,待日后再来详细总结这方面的内容(TODO)。

其他单例模式实现方式

除了上述方式,还有些其他的方式也可以实现单例模式。

静态内部类实现

代码如下:

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

这种方式由JVM保证其在多线程环境下的线程安全。同时,这种实现方式看似是饿汉式的,实际上根据JVM虚拟机类初始化规则,只有当用到时才会被初始化。此部分涉及JVM虚拟机规范,待日后完善(TODO)。

枚举实现

枚举类写法简单,且无需考虑线程安全问题,也是一种不错的实现方式。
示例代码如下:

public enum SingletonEnum {
    UNIQUE_INSTANCE("Unique Instance!");

    // 下面可以当成一个正常的类来使用
    private String name;

    SingletonEnum(String name) {
        this.name = name;
    }

    public void print() {
        System.out.println(STR."My name is \{name}");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) {
        SingletonEnum.UNIQUE_INSTANCE.print();
    }
}

文章作者: Serendipity
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 闲人亭
设计模式 Java 设计模式
喜欢就支持一下吧