likes
comments
collection
share

【Java设计模式001】单例模式

作者站长头像
站长
· 阅读数 14

前言

大家好,我们的gzh是朝阳三只大明白,满满全是干货,分享近期的学习知识以及个人总结(包括读研和IT),跪求一波关注,希望和大家一起努力、进步!!

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。如果我们需要让某一个类在整个程序生命周期内只能有一个实例,那么就要使用单例模式。

想要实现单例模式,必须满足三个必要条件:

  1. 单例类的构造器是私有的,客户端无法通过 new 关键字创建实例;
  2. 单例类必须自己创建自己的唯一实例;
  3. 单例类必须给客户端提供一个方法以获取到唯一实例;

实现

单例设计模式一般分为如下两类:

  • 饿汉式:类加载就会导致该单实例对象被创建;
  • 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会被创建。

实现的基本思路如下:

  1. 构造器私有化;
  2. 类的内部创建对象;
  3. 向外暴露静态 getInstance 方法;

懒汉式-静态变量法

静态变量法是最基本的一个实现,通过静态变量的方式创建唯一对象,其实现如下:

public class Singleton1 {

    // 类的内部创建对象;
    private static Singleton1 instance = new Singleton1();

    // 构造器私有化
    private Singleton1() {
    }

    // 向外暴露静态方法
    public static Singleton1 getInstance() {
        return instance;
    }
}

除此之外还有一种使用静态代码块的构建方法

public class Singleton2 {
    private static Singleton2 instance;

    // 使用静态代码块创建静态示例;
    static {
        instance = new Singleton2();
    }

    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        return instance;
    }
}
  • 优势:写法简单,避免了多线程的问题;
  • 劣势:没有实现懒加载的效果,这种方式基于classloder机制避免了多线程的同步问题,但会造成不必要的内存浪费;

懒汉式-线程不安全实现

为了解决饿汉式类加载造成的内存浪费,可以采用懒加载的方式创建实例,以下是懒汉式的最基本实现。

public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {
    }

    // 可能出现线程安全问题
    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }

        return instance;
    }
}

这种方式是最基本的实现方式,其最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式不要求线程安全,在多线程不能正常工作。

懒汉式-线程安全

由于懒汉式基础实现有多线程安全问题,因此最简单的解决方案就是加锁,其实现如下:

public class Singleton4 {

    private static Singleton4 instance;

    private Singleton4() {
    }

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

        return instance;
    }
}

这种方式能够在多线程中很好的工作,同时能够实现懒加载。但是锁的粒度很大,同步效率低。

懒汉式-双重检查

public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {
    }

    // 双重检查
    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                if (instance == null) {
                    instance = new Singleton5();
                }
            }
        }

        return instance;
    }
}

双重检查实现方式既能保证线程安全,也能实现懒加载,同时其效率较高,在多线程情况下能保持高性能。

懒汉式-静态内部类实现

使用静态内部类的优点在于:

  1. 当一个类被加载的时候,静态内部类不会被加载;
  2. classloader会保证静态内部类加载时线程安全。

使用静态内部类可以天然的实现线程安全的懒加载单例类,其具体实现如下:

public class Singleton6 {

    private static Singleton6 instance;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        return InnerSingleton6.instance;
    }

    // 使用静态内部类实现单例
    public static final class InnerSingleton6 {
        private static Singleton6 instance = new Singleton6();
    }
}

类加载器加载 Singleton6时不定会加载 InnerSingleton6 只有显式的调用 getInstance 方法时才会加载内部类,从而实例化 instance。这种实现由 JVM 保证类加载时的线程安全,效率非常高。

使用枚举类

这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。其唯一的问题在于枚举类永远继承自 Enum,且无法被继承;

public enum Singleton7 {

    INSTANCE;


    public void otherMethod() {
        // ....
        return;
    }
}

上面的这种实现是一种饿汉式实现,也可以将业务类放在一个枚举类中:

public enum Singleton8 {
    INSTANCE;
    private User user;

    Singleton8() {
        user = new User();
    }

    public User getUser() {
        return user;
    }

    public static  class User {

    }
}

破坏单例模式

序列化和反序列化

可以使用序列化和反序列破坏单例模式,以下面单例类为例:

public class Singleton1 implements Serializable {

    // 类的内部创建对象;
    private static Singleton1 instance = new Singleton1();

    // 构造器私有化
    private Singleton1() {
    }

    // 向外暴露静态方法
    public static Singleton1 getInstance() {
        return instance;
    }
}

测试类:

public class Main {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton1 oriInstance = Singleton1.getInstance();
        // 创建对象输出流并输出对象
        ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("obj.txt")));
        oos.writeObject(oriInstance);
        // 释放资源
        oos.close();

        // 创建对象输入流并读取
        ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("obj.txt")));
        Singleton1 readInstance = (Singleton1) ois.readObject();
        // 释放资源
        ois.close();

		// false
        System.out.println(oriInstance == readInstance);
    }
}

解决方案:在单例类中定义一个 readResolve 方法,该方法在反序列化时被调用;以 ObjectInputStream 源码为例:

class ObjectInputStream{
    private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);
		
        // 如果有readResolve方法,则调用该方法
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }
}

反射

通过反射同样可以破坏单例模式,其测试类如下:

public class test {
    public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        // 获取字节码文件
        Class<Singleton1> singleton1Class = Singleton1.class;
        // 获取获取无参构造器
        Constructor<Singleton1> constructor = singleton1Class.getDeclaredConstructor();
        // 设置访问权限
        constructor.setAccessible(true);

        Singleton1 singleton1 = constructor.newInstance();
        Singleton1 singleton2 = constructor.newInstance();

		// false
        System.out.println(singleton1 == singleton2);
    }
}

解决方案:在私有构造器中加上一个判断,如果对象二次创建,则直接抛出异常。结合防止序列化和反序列化的方式,单例类的代码修改如下:

public class Singleton1 implements Serializable {

    // 类的内部创建对象;
    private static Singleton1 instance = new Singleton1();

    // 放置反射破坏单例
    private static boolean uniqueInstance = false;

    // 构造器私有化
    private Singleton1() {
        if (uniqueInstance) {
            throw new RuntimeException("重复构建对象");
        }

        uniqueInstance = true;
    }

    // 向外暴露静态方法
    public static Singleton1 getInstance() {
        return instance;
    }
    
    // 放置序列化反序列化破坏单例
    public Object readResolve() {
        return instance;
    }
}

总结

一般情况下,使用静态变量法实现饿汉式;如果有实现懒加载的需求时使用静态内部类的方式进行实现;如果涉及到反序列化创建对象时,可以尝试使用枚举方式。最后才考虑使用双重检查的方式实现单例。

文中难免会出现一些描述不当之处(尽管我已反复检查多次),欢迎在留言区指正,相关的知识点也可进行分享,希望大家都能有所收获!!如果觉得我的文章写得还行,不妨支持一下。你的每一个转发、关注、点赞、评论都是对我最大的支持!