设计模式系列-单例模式

362 阅读4分钟

个人技术博客(IBLi) CSDN Github 掘金

单例模式

概念

单例模式:指一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点(getInstance方法)。 大概实现就是隐藏其构造方法,单例模式属于创建型模式。
一些实际的应用场景比如,DBpool, ServletContext,ServletConfig等

单例模式写法

饿汉式单例

在单例类首次加载时创建实例;

public class HungrySingleton {
    private static final HungrySingleton hungrySingleton = new HungrySingleton();

    private HungrySingleton(){}

    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

优点
1、执行效率高,没有加任何锁

缺点
1、类加载的时候就初始化,在某些情况下,可能会造成内存浪费;如果出现类的数量很多的时候,会初始化很多类,占用大量内存;

局限性
Spring就不能使用,Spring启动的时候,会有大量的类加载。

饿汉式的第二种写法

public class HungryStaticSingleton {

    private static final HungryStaticSingleton hungrySingleton ;

    static {
        hungrySingleton = new HungryStaticSingleton();
    }

    private HungryStaticSingleton(){}

    public static HungryStaticSingleton getInstance(){
        return hungrySingleton;
    }
}

区别仅仅实在与类加载的顺序不同。

懒汉式单例

被外部类调用时才创建实例;

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
    }

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

优点
1、节省了内存 缺点 1、不能保证真实单例,线程不安全

线程不安全的原因有两种

  • 后面的线程覆盖掉前面线程创建的实例
  • 同时进入判断条件,按顺序返回,没有覆盖的时候,就返回实例

解决方法:

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

但是getInstance方法添加上锁之后,性能下降,如果有很多请求访问,除了获得锁的线程之外,其他线程都要等待。

如何优化?

public class LazyDclSingleton {

    private static LazyDclSingleton instance = null;

    private LazyDclSingleton() {
    }
    // 后面的线程覆盖掉前面线程创建的实例
    public static LazyDclSingleton getInstance() {
        if (instance == null) {
            synchronized (LazyDclSingleton.class) {
                instance = new LazyDclSingleton();
            }
        }
        return instance;
    }
}

双重检查锁

public class LazyDclSingleton {

    private static volatile LazyDclSingleton instance = null;

    private LazyDclSingleton() {
    }

    public static LazyDclSingleton getInstance() {
        // 检查是否要阻塞
        if (instance == null) {
            synchronized (LazyDclSingleton.class) {
                // 检查是否要创建实例
                if (instance == null) {
                    instance = new LazyDclSingleton();
                }
            }
        }
        return instance;
    }
}

局限性 会出现指令重排序的问题,有可能返回一个不完整的实例 解决方案:private static volatile LazyDclSingleton instance = null;(volatile禁止指令重排序)

双重检查锁的优缺点

优点
1、性能高,能保证线程安全
缺点
1、代码可读性查,不够美观,代码不够优雅

静态内部类写法

/**
 * 静态内部类
 * 静态内部类在使用时才进行构建
 * classPath: ../LazyInnerClassSingleton.class
 *            ../LazyInnerClassSingleton$LazyHolder.class
 *
 * 优点:写法优雅,利用了java语言语法,性能也高,避免内存的浪费
 * 缺点:能够被反射破坏
 */
public class LazyInnerClassSingleton {
    private LazyInnerClassSingleton(){}
    public static LazyInnerClassSingleton getInstance(){
        return LazyHolder.instance;
    }

    private static class LazyHolder{
        private static final LazyInnerClassSingleton instance = new LazyInnerClassSingleton();
    }
}

优点
1、写法优雅,利用了java语言语法
2、性能也高
3、避免内存的浪费
缺点
1、能够被反射破坏单例

/**
 * 反射破坏单例模式
 */
public class ReflectTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<?> clazz = LazyInnerClassSingleton.class;
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        Object instance = declaredConstructor.newInstance();
        System.err.println(instance);
    }
}
// 打印结果 com.ibli.javaBase.pattern.singleton.LazyInnerClassSingleton@38af3868

解决办法: 在构造器中添加一个判断,如果实例已经创建,则直接抛出异常终止创建;

 private LazyInnerClassSingleton(){
        if (LazyHolder.instance != null){
            throw new IllegalArgumentException();
        }
    }

注册式单例

将每一个实例都缓存到一个容器中,使用唯一标志获取实例

枚举写法

public enum  EnumSingleton  {

    INSTANCE;

    private Object object;

    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

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

使用与测试

public class EnumSingletonTest {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        EnumSingleton enumSingleton =   EnumSingleton.getInstance();
        enumSingleton.setObject(new Object());

        // 尝试使用反射破坏
        Class<?> clazz = EnumSingleton.class;
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(String.class,int.class);
        System.err.println(declaredConstructor);
        declaredConstructor.setAccessible(true);
        Object object = declaredConstructor.newInstance();
        System.err.println(object);
    }
}

测试结果:

private com.ibli.javaBase.pattern.singleton.EnumSingleton(java.lang.String,int)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.ibli.javaBase.pattern.singleton.EnumSingletonTest.main(EnumSingletonTest.java:17)

原因在JDK底层源码中已经做了限制

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        // 不能创建枚举类型
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

枚举写法优缺点

优点
1、写法优雅,使用方便
缺点
2、和饿汉式一样,在某些情况下会造成大量内存浪费

容器式单例写法

public class ContainerSingleton {

    private ContainerSingleton() {
    }
    
    private static Map<String, Object> ioc = new ConcurrentHashMap<>();

    public static Object getInstance(String className) {
        Object instance = null;
        if (!ioc.containsKey(className)) {
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return instance;
        } else {
            return ioc.get(className);
        }
    }
}

测试类:

public class ContainerSingletonTest {
    public static void main(String[] args) {
        Object o1 = ContainerSingleton.getInstance("com.ibli.javaBase.pattern.singleton.Pojo");
        Object o2 = ContainerSingleton.getInstance("com.ibli.javaBase.pattern.singleton.Pojo");
        System.err.println(o1 == o2); // true
    }
}

容器式单例写法适合创建大量单例实例的场景,类似与Spring的IOC容器。
当然上面的写法也会存在一个线程安全问题; // TODO

序列化破坏单例模式

/**
 * 序列化:把内存中对象的状态转换为字节码的形式,然后在把字节码以IO输出流写到磁盘上
 * 反序列化: 将持久化的字节码内容,通过IO流的方式读取到内存中,然后在转换成Java对象
 */
public class SerializableSingleton implements Serializable {

    private static final SerializableSingleton serializableSingleton = new SerializableSingleton();

    private SerializableSingleton() {
    }

    public static SerializableSingleton getInstance() {
        return serializableSingleton;
    }
}

测试类:

public class SerializableSingletonTest {
    public static void main(String[] args) {
        SerializableSingleton s1;
        SerializableSingleton s2 = SerializableSingleton.getInstance();

        FileOutputStream fos;
        try {
            fos = new FileOutputStream("SerializableSingleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();


            FileInputStream fis = new FileInputStream("SerializableSingleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (SerializableSingleton) ois.readObject();
            ois.close();

            System.out.println(s1);
            System.out.println(s2);
            System.out.println(s1 == s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

结果:

com.ibli.javaBase.pattern.singleton.SerializableSingleton@20ad9418
com.ibli.javaBase.pattern.singleton.SerializableSingleton@681a9515
false

解决方法:在SerializableSingleton中添加一个方法

    // 桥接模式
    private Object readResolve() {
        return serializableSingleton;
    }

结果:

com.ibli.javaBase.pattern.singleton.SerializableSingleton@681a9515
com.ibli.javaBase.pattern.singleton.SerializableSingleton@681a9515
true

原因:ois.readObject();方法底层有对readResolve方法的判断,如果不存在这个方法,会利用反射生成一个新的实例;

ThreadLocal单例

下面介绍一种比较少见的一种单例模式

public class ThreadLocalSingleton {

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

    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalSingleton.get();
    }
}
public class ThreadLocalExector implements Runnable{
    @Override
    public void run() {
        System.err.println(ThreadLocalSingleton.getInstance());
    }
}

测试:

public class ThreadLocalSingletonTest {

    public static void main(String[] args) {
        System.out.println(ThreadLocalSingleton.getInstance());
        System.out.println(ThreadLocalSingleton.getInstance());

        Thread thread1 = new Thread(new ThreadLocalExector());
        Thread thread2 = new Thread(new ThreadLocalExector());
        thread1.start();
        thread2.start();;
    }
}

结果:

com.ibli.javaBase.pattern.singleton.ThreadLocalSingleton@38af3868  1
com.ibli.javaBase.pattern.singleton.ThreadLocalSingleton@38af3868  2

com.ibli.javaBase.pattern.singleton.ThreadLocalSingleton@10c69a60  3
com.ibli.javaBase.pattern.singleton.ThreadLocalSingleton@2f1eeb2f  4

以上3和4的结果虽然不一样,但是其实也是实现了【单例】的效果。

山脚太拥挤 我们更高处见。