3,单例模式

57 阅读11分钟

1,前言

前边已经说了迭代器和组合,今天说单例模式
大多数人可能会认为单例模式是最简单的设计模式,没有之一
好吧,我承认单例确实很简单,但似乎还没有简单到大多数人想的那样
所以第一篇没有说单例,放到第四篇才拿出来

2,单例模式

单例模式:保证一个类仅有一个实例,并提供一个全局访问点

通过new关键字实例化一个类显然是不能保证这个类仅有一个实例存在的
我们需要做三件事:
    1,私有化无参构造函数,阻止外部对其进行实例化
    2,一个静态变量保存对象唯一实例
    3,一个静态方法提供外部获取此单例
package com.brave.singleton_lazy;

/**
 * 1,懒汉式单例模式-线程不安全:
 *  保证一个类仅有一个实例,并提供一个全局访问点
 *  优点:使用时才进行实例化
 *  缺点:线程不安全,多线程下不能正常工作
 * 
 * @author Brave
 *
 */
public class Singleton_Lazy {

    // 静态单例对象
    private static Singleton_Lazy instance = null;

    // 将默认的无参构造函数变为私有,阻止通过外部new关键字创建对象
    private Singleton_Lazy(){

    }

    // 对外提供一个全局访问点,用于请求获取单例对象
    // 首次获取实例时,才会进行实例化
    public static Singleton_Lazy getInstance(){

        if(instance == null){
            instance = new Singleton_Lazy();
        }

        return instance;
    }

    // 其他方法...

}
package com.brave.singleton_lazy;

public class Client {

    public static void main(String[] args) {

        Singleton_Lazy instanceA = Singleton_Lazy.getInstance();
        Singleton_Lazy instanceB = Singleton_Lazy.getInstance();

        /**
         * "=="相当于".equals"
         * 对于值类型会判断其值是否相等
         * 对于引用类型会判断是否为同一个引用(即内存地址是否相同)
         */
        if(instanceA == instanceB){
            System.out.println("A和B是同一个对象");
        }else{
            System.out.println("A和B不是同一个对象");
        }

    }

}

以上是一个懒汉式单例模式,也是最常见的一种,首次获取实例时创建对象,测试结果:A和B是同一个对象

注意:在JVM 1.2之前版本,由于垃圾收集器BUG,会造成单例在没有全局引用时被当做垃圾清除,因此在1.2之前需要一个全局引用(单例注册表)来保护单例不被回收


3,懒汉式单例模式的问题

懒汉式的单例模式并不是线程安全的,因为以下这段代码在多线程下会出现问题:
    public static Singleton_Lazy getInstance(){

        if(instance == null){
            instance = new Singleton_Lazy();
        }

        return instance;
    }
当线程A进入if判断,且尚未实例化完成时,线程B进入if判断,此时会new出两个实例,因此在多线程下以上懒汉式单例无法正常工作

4,线程安全的单例模式

1,饿汉式单例模式

package com.brave.singleton_hunger;

/**
 * 2,饿汉式单例模式-线程安全:
 *  保证一个类仅有一个实例,并提供一个全局访问点
 *  相比于懒汉式单例,饿汉式单例模式在静态初始化时完成单例对象的实例化
 *  缺点:不管是否使用这个单例,它都会一直在内存中
 *  饿汉式单例模式是线程安全的单例模式
 *  
 * @author Brave
 *
 */
public class Singleton_Hunger {

    // 静态单例对象-静态初始化时就会创建单例对象
    private static Singleton_Hunger instance = new Singleton_Hunger();

    // 将默认的无参构造函数变为私有,阻止通过外部new关键字创建对象
    private Singleton_Hunger(){

    }

    // 对外提供一个全局访问点,用于请求获取单例对象
    // 由于在静态初始化时就已完成实例化,所以饿汉式单例模式是线程安全的
    public static Singleton_Hunger getInstance(){
        return instance;
    }

    // 其他方法...

}
相比于懒汉式单例,饿汉式单例模式由于在静态初始化时就创建了实例(由JVM的静态初始化器控制),
获取实例时也就不必再去判断是否需要创建实例,所以也就没有了线程安全问题的存在

2,双重校验锁单例模式

饿汉式单例确实解决了线程安全问题,但是相比于懒汉式,它牺牲了延迟实例化这个优点,
那么可不可以即可以延迟实例化又是线程安全的单例模式呢?

这里有两种写法都是同步锁的思想:

    static Lock lock = new ReentrantLock();
    public static Singleton getInstance(){
        lock.lock();
        if(instance == null){
            instance = new Singleton();
        }
        lock.unlock();
        return instance;
    }
    public static synchronized Singleton getInstance() {  

        if (instance == null) {  
            instance = new Singleton();  
        }  

        return instance;  
    }
这种加锁或同步的方式可以多线程下控制同一时间只有一个线程能去做实例化,从而有效解决线程安全问题

但是使用这种方式解决线程安全问题的代价是使这段代码的运行效率降低100倍

所以,为了既能解决线程安全问题,还能兼顾效率,就有了双重检验锁的单例模式,简称双锁单例模式

package com.brave.singleton_doublelock;

/**
 * 3,双重校验锁单例模式-线程安全
 *  保证一个类仅有一个实例,并提供一个全局访问点
 *  JDK1.5之后,双重检查锁定才能够正常达到单例效果。
 * @author Brave
 *
 */
public class Singleton_DoubleLock {

    // 确保instance实例化时,多个线程正确处理instance变量
    private volatile static Singleton_DoubleLock instance = null;

    // 将默认的无参构造函数变为私有,阻止通过外部new关键字创建对象
    private Singleton_DoubleLock(){

    }  

    // 先检查实例是否被创建,未创才同步,所以只有第一次会进行同步
    // 创建实例前再做一次校验,若此时仍是null,才会实例化对象
    public static Singleton_DoubleLock getInstance() {  

        if (instance == null) {  

            synchronized (Singleton_DoubleLock.class) {  

                if (instance == null) {  

                    instance = new Singleton_DoubleLock();  

                }  

            }  
        } 

        return instance;  
    }  

    // 其他方法...

}
双重校验锁单例模式:每次外部请求单例对象时,先检查是否已实例化,如果尚未实例化才进行实例化
理论上只有第一次才会彻底执行同步区块内的代码,既实现了延迟加载,又是线程安全的单例模式

注意:java 5之前(1.4及更早版本),volatile关键字的问题会导致双锁单例模式失效

3,静态内部类单例模式

此方法通过java的类级内部类和多线程缺省同步锁,巧妙的同时实现了延迟加载和线程安全。

package com.brave.singleton_innerclass;

/**
 * 静态内部类单例模式-线程安全
 *  保证一个类仅有一个实例,并提供一个全局访问点
 *  静态内部类单例模式也是一种懒加载方式
 *  线程安全,调用效率高,且实现了延迟加载
 * 
 * @author Brave
 *
 */
public class Singleton_InnerClass {

    // 将默认的无参构造函数变为私有,阻止通过外部new关键字创建对象
    private Singleton_InnerClass(){

    }  

    // 通过内部类实例化单例
    private static class SingletonClassInstance {  
        // 静态初始化器,由JVM保证线程安全
        private static final Singleton_InnerClass instance = new Singleton_InnerClass();  
    }  

    // 首次调用才会实例化单例对象
    public static Singleton_InnerClass getInstance() {  
        return SingletonClassInstance.instance;  
    }  

    // 其他方法...

}
回想一下前边提到的饿汉式单例,虽然线程安全,但类装载时创建对象会浪费内存
而静态内部类单例模式正好克服了这一点:
    采用类级内部类(只有在第一次被使用的时候才会被装载),类装载的时候不初始化对象
    使用JVM静态初始化器进行初始化变量时,JVM会隐含为我们执行synchronized进行加互斥锁的同步控制
    从而同时实现延迟加载和线程安全

4,枚举单例模式

这个不多说了,枚举是天然的单例模式,线程安全但不能实现延迟加载

package com.brave.singleton_enum;

/**
 * 5,枚举单例模式-线程安全
 * 优点:线程安全,调用效率高,天然防止反射和反序列化漏洞 
 * 缺点:不能实现延迟加载
 * 
 * @author Brave
 *
 */
public enum Singleton_Enum {

    // 枚举元素,本身就是单例对象  
    INSTANCE;  

    // 可以添加自己需要的操作  
    public void singletonEnumMethod() {
        System.out.println("调用枚举方法");  
    }  

}

5,单例模式的反射问题

我们以饿汉式单例模式为例:

package com.brave.singleton_hunger;

/**
 * 2,饿汉式单例模式-线程安全:
 *  保证一个类仅有一个实例,并提供一个全局访问点
 *  相比于懒汉式单例,饿汉式单例模式在静态初始化时完成单例对象的实例化
 *  缺点:不管是否使用这个单例,它都会一直在内存中
 *  饿汉式单例模式是线程安全的单例模式
 *  
 * @author Brave
 *
 */
public class Singleton_Hunger {

    // 静态单例对象-静态初始化时就会创建单例对象
    private static Singleton_Hunger instance = new Singleton_Hunger();

    // 将默认的无参构造函数变为私有,阻止通过外部new关键字创建对象
    private Singleton_Hunger(){

    }

    // 对外提供一个全局访问点,用于请求获取单例对象
    // 由于在静态初始化时就已完成实例化,所以饿汉式单例模式是线程安全的
    public static Singleton_Hunger getInstance(){
        return instance;
    }

    // 其他方法...

}
package com.brave.singleton_hunger;

import java.lang.reflect.Constructor;

public class Client {

    public static void main(String[] args) throws Exception {

        /**
         *  反射问题
         */
        // 正常测试
        System.out.println(Singleton_Hunger.getInstance());
        System.out.println(Singleton_Hunger.getInstance());

        // 反射测试
        Class<Singleton_Hunger> clazz = (Class<Singleton_Hunger>) Class.forName("com.brave.singleton_hunger.Singleton_Hunger");
        Constructor<Singleton_Hunger> con = clazz.getDeclaredConstructor(null);
        con.setAccessible(true); // 构造函数访问权限

        System.out.println(con.newInstance());
        System.out.println(con.newInstance());

    }

}

输出结果:

// 正常测试
com.brave.singleton_hunger.Singleton_Hunger@74a14482
com.brave.singleton_hunger.Singleton_Hunger@74a14482

// 反射测试
com.brave.singleton_hunger.Singleton_Hunger@1540e19d
com.brave.singleton_hunger.Singleton_Hunger@677327b6

反射问题的解决方法:

调用构造方法时,先判断单例是否已经存在,若已存在则不再创建新的实例
    private Singleton_Hunger(){
        // 此段代码解决单例反射问题,如果已经实例化,不再允许创建新对象
        if(instance != null) {
            throw new RuntimeException();
        }
    }

6,单例模式的反序列化问题

还是以饿汉式单例模式为例

单例模式实现Serializable序列化

package com.brave.singleton_hunger;

import java.io.Serializable;

/**
 * 2,饿汉式单例模式-线程安全:
 *  保证一个类仅有一个实例,并提供一个全局访问点
 *  相比于懒汉式单例,饿汉式单例模式在静态初始化时完成单例对象的实例化
 *  缺点:不管是否使用这个单例,它都会一直在内存中
 *  饿汉式单例模式是线程安全的单例模式
 *  
 * @author Brave
 *
 */
public class Singleton_Hunger_Serializable implements Serializable {

    // 静态单例对象-静态初始化时就会创建单例对象
    private static Singleton_Hunger_Serializable instance = new Singleton_Hunger_Serializable();

    // 将默认的无参构造函数变为私有,阻止通过外部new关键字创建对象
    private Singleton_Hunger_Serializable(){

    }

    // 对外提供一个全局访问点,用于请求获取单例对象
    // 由于在静态初始化时就已完成实例化,所以饿汉式单例模式是线程安全的
    public static Singleton_Hunger_Serializable getInstance(){
        return instance;
    }

    // 其他方法...

}

测试反序列化:

package com.brave.singleton_hunger;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Client_Serializable {

    public static void main(String[] args) throws Exception {

        // 正常测试
        Singleton_Hunger_Serializable Singleton1 = Singleton_Hunger_Serializable.getInstance();
        Singleton_Hunger_Serializable Singleton2 = Singleton_Hunger_Serializable.getInstance();
        System.out.println("Singleton1 = " + Singleton1); 
        System.out.println("Singleton2 = " + Singleton2); 

        // 反序列化测试
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\test.txt"));
        oos.writeObject(Singleton1);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\test.txt"));
        Singleton_Hunger_Serializable Singleton3 = (Singleton_Hunger_Serializable) ois.readObject();
        ois.close();

        System.out.println("反序列化 Singleton = " + Singleton3);

    }
}

测试结果:

Singleton1 = com.brave.singleton_hunger.Singleton_Hunger_Serializable@74a14482
Singleton2 = com.brave.singleton_hunger.Singleton_Hunger_Serializable@74a14482

反序列化 Singleton = com.brave.singleton_hunger.Singleton_Hunger_Serializable@45ee12a7
我们发现经过反序列化拿到的对象不再是之前的单例对象了

反序列化问题的解决方法:

单例中添加readResolve方法,直接返回对象

    // 解决单例模式的反序列化问题
    // 反序列化时直接返回对象,不再重新创建
    private Object readResolve() {
        return instance;
    } 

修改后结果:

Singleton1 = com.brave.singleton_hunger.Singleton_Hunger_Serializable@74a14482
Singleton2 = com.brave.singleton_hunger.Singleton_Hunger_Serializable@74a14482

反序列化 Singleton = com.brave.singleton_hunger.Singleton_Hunger_Serializable@74a14482

7,单例的执行效率

此方法可以模拟多线程下单例各单例模式的执行效率(数值不准确,多次误差较大)

package com.brave.singleton_pk;

import java.util.concurrent.CountDownLatch;

import com.brave.singleton_doublelock.Singleton_DoubleLock;
import com.brave.singleton_enum.Singleton_Enum;
import com.brave.singleton_hunger.Singleton_Hunger;
import com.brave.singleton_innerclass.Singleton_InnerClass;
import com.brave.singleton_lazy.Singleton_Lazy;

/**
 * 各种单例的效率测试
 * @author Brave
 *
 */
public class Client {

    public static void main(String[] args) throws Exception {

        int threadNum = 10000;  // 启10000个线程
        long beginTimeMillis = System.currentTimeMillis();  // 开始时间
        long endTimeMillis = 0; // 完成时间

        // CountDownLatch:使一个线程等待其他线程完成各自的工作后再执行
        final CountDownLatch countDownLatch = new CountDownLatch(threadNum);  

        // 循环创建100个线程,用于测试几种单例的运行效率
        for (int i = 0; i < threadNum; i++) {
            new Thread(new Runnable() {
                @Override  
                public void run() {  
                    for (int i = 0; i < 50000; i++) {
//                      Object instance1 = Singleton_Lazy.getInstance(); // 768.懒汉式
//                      Object instance2 = Singleton_Hunger.getInstance(); // 688.饿汉式  
//                      Object instance3 = Singleton_DoubleLock.getInstance(); // 682.双重检查锁
//                      Object instance4 = Singleton_InnerClass.getInstance(); // 594.静态内部类  
//                      Object instance5 = Singleton_Enum.INSTANCE; // 565.枚举单例 
                    }  
                    countDownLatch.countDown();  //此线程完成后,计数器倒数一次
                }  
            }).start();  
        }  

        // 等待所有线程执行完成(countDownLatch倒数为0,否则阻塞等待)
        countDownLatch.await();

        endTimeMillis = System.currentTimeMillis();

        System.out.println("总耗时:" + (endTimeMillis - beginTimeMillis));  

    }

}

8,结尾

没想到这篇居然写了3个多小时,现在是夜里3点15分

其实我的水平很有限,一大半的原因还是自己的忘性比较大,写下来做个备忘

希望能和大家一起分享一起交流,如果碰巧我的博文帮到了你,那会让我感到非常荣幸

不多说了,明天计划写工厂模式,希望不要像今天一样

哦,对了,我们一共说了5种单例:饿汉,懒汉,双重校验锁,静态内部类,枚举