详细说说单例模式

Posted 轻舸泛舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详细说说单例模式相关的知识,希望对你有一定的参考价值。

单例模式

特点:全局唯一,在整个程序中,只有一个对象。

什么样的类适合单例?

  • 全局使用的类
  • 创建和销毁会消耗很多系统资源的类
    • 数据库连接池
    • 工厂类
    • 数据源

应用:

  • Spring的Bean默认情况下是单例
  • 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。
  • 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  • Application 也是单例的典型应用(Servlet编程中会涉及到)
  • 在servlet编程中,每个Servlet也是单例
  • 在spring MVC框架/struts1框架中,控制器对象也是单例

饿汉式

优点:

  • 类一加载就创建实例,没有延迟
  • 线程安全

缺点:

  • 反射和反序列化会破坏单例
  • 如果没有用到该类就会产生内存浪费
  • 如果加载的资源很大,程序启动的时候就会产生效率问题
class Singleton implements Serializable {
    private Singleton() {
    }

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

反射破坏单例:

@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Singleton instance = Singleton.getInstance();
    Class<Singleton> clazz = Singleton.class;
    Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton instance1 = constructor.newInstance();
    System.out.println(instance == instance1); // false
}

反序列化破坏单例:

@Test
public void test() throws IOException, ClassNotFoundException {
    Singleton i1 = Singleton.getInstance();
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./singleton.obj"));
    out.writeObject(i1);
    out.close();

    FileInputStream in = new FileInputStream("./singleton.obj");
    ObjectInputStream ois = new ObjectInputStream(in);
    Singleton i2 = (Singleton) ois.readObject();
    System.out.println(i1);
    System.out.println(i2);
    // com.qianyu.thread.Singleton@6842775d
    // com.qianyu.thread.Singleton@1ae369b7
}

加入readResolve方法解决反序列化问题

import java.io.*;

class Singleton implements Serializable {
    private Singleton() {
    }

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public Object readResolve() {
        return INSTANCE;
    }
}

为了防止反射破坏单例,我们可以在构造器中添加判断

import java.io.*;

class Singleton implements Serializable {
    private Singleton() {
        if (INSTANCE != null) {
            throw new RuntimeException("不能通过反射创建对象");
        }
    }

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

懒汉式

特点:

  • 延时加载
  • 线程不安全
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

模拟线程不安全情况:

@Test
public void test() throws InterruptedException {
    for (int i = 0; i < 20; i++) {
        new Thread(() -> System.out.println(Singleton.getInstance())).start();
    }
    Thread.sleep(1000);
}

使用同步解决线程不安全问题

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

    private Singleton() {
    }

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

双重检测锁模式

懒汉式的单例模式虽然可以使用synchronized解决线程不安全问题,但是每次获取单例对象的时候都要执行synchronized代码块,造成效率低,为了解决这个问题,提出双重检测锁模式

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

指令重排问题

instance = new Singleton()会执行如下操作:

  1. 分配内存空间
  2. 初始化对象
  3. instance执行(1)中分配的空间

但是在某些编译器上,可能会发生指令重排:

  1. 分配内存空间
  2. instance执行(1)中分配的空间(但此时对象没有初始化)
  3. 初始化对象

这样的话,如果多个线程同时访问的话,有可能会出现某些对象为空的情况。为了防止JVM进行指令重排,我们可以加上volatile关键字

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

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

登记式

登记式,也可以叫做“静态内部类”式。

首先我们要知道:

  • 只有调用内部类的时候,内部类才开始加载(延时加载)
  • 非静态内部类要依附于外部类,也就是说外部类必须创建对象才能使用非静态内部类
  • 静态内部类不用外部类创建对象就可以使用
class Test {
    class Inner {
    }

    static class SInner {
    }
}

public class Demo {
    public static void main(String[] args) {
        Test.Inner inner = new Test().new Inner();
        Test.SInner s = new Test.SInner();
    }
}

登记式相对于饿汉式的好处:

  • 可以实现延迟加载
  • 可以通过改造,使其对反射安全
public class Singleton {
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    // 如果使用反射调用会报错
    private Singleton() {
        if (SingletonHolder.instance != null) {
            throw new IllegalStateException();
        }
    }

    public Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

枚举式

特点:

  • java1.5之后出现
  • 目前推荐实现单例的最佳方式
  • 线程安全
  • 立即初始化
  • 自动支持序列化,防止反序列化创建新的对象
  • 防止反射攻击
public enum Singleton {
    INSTANCE{
        @Override
        protected void dosomething() {
            System.out.println("Singleton.dosomething");
        }
    };

    protected abstract void dosomething();
}

反射调用构造器,会抛出异常:

@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Class<Singleton> clazz = Singleton.class;
    Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton s1 = constructor.newInstance();
    // java.lang.NoSuchMethodException: com.qianyu.thread.Singleton.<init>()
    //  at java.lang.Class.getConstructor0(Class.java:3082)
    //  at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    //  at com.qianyu.thread.Application.test(Application.java:11)
}

ThreadLocal

优点:

  • 空间换时间
  • 延时加载

缺点:

  • 只能是在同一个线程中获得的两个对象才是单例

代码实现:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    private static ThreadLocal<Singleton> threadLocalSingleton = new ThreadLocal<Singleton>() {
        @Override
        protected Singleton initialValue() {
            return new Singleton();
        }
    };

    public static Singleton getInstance() {
        return threadLocalSingleton.get();
    }
}

多线程环境模拟

@Test
public void test() throws InterruptedException {
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            Singleton s1 = Singleton.getInstance();
            Singleton s2 = Singleton.getInstance();
            System.out.println("s2 = " + s2 + " , s1 = " + s1);

        }).start();
    }
    Thread.sleep(1000);
}

CAS

缺点:

  • 可能会产生垃圾对象

代码实现

import java.util.concurrent.atomic.*;

public class Singleton {
    private static final AtomicReference<Singleton> instance = new AtomicReference<>();

    private Singleton() {
    }

    public static final Singleton getInstance() {
        while (true) {
            Singleton current = instance.get();
            if (current != null) {
                return current;
            }
            current = new Singleton();
            if (instance.compareAndSet(null, current)) {
                return current;
            }
        }
    }
}

总结

单例实现要点:

  1. 私有构造器
  2. 持有该类的属性
  3. 对外提供获取实例的静态方法

上述几种单例模式的比较

  • 饿汉式:线程安全、反射不安全、反序列化不安全、非延时加载
  • 懒汉式:线程不安全、反射不安全、反序列化不安全、延时加载
  • 双重检测锁:线程安全、反射不安全、反序列化不安全、延时加载
  • 登记时:线程安全、反射安全、反序列化不安全、延时加载
  • 枚举式:线程安全、反射安全、反序列化安全、非延时加载
  • ThreadLocal:不加锁,以空间换时间,为每个线程提供独立副本,可以保证各自线程是单例的,但是不同线程之间不是单例的
  • CAS:无锁乐观策略,线程安全


以上是关于详细说说单例模式的主要内容,如果未能解决你的问题,请参考以下文章

面试官:说说对单例模式的理解,最后的枚举实现我居然不知

单例模式基类随笔

急求java设计模式,要求用到两种设计模式 求详细代码

面试官所认为的单例模式

单例模式的C++实现(懒汉模式和饿汉模式的详细讲解和实现)

单例模式的C++实现(懒汉模式和饿汉模式的详细讲解和实现)