ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

单例模式有这么多种写法(JAVA单例模式浅析)

2021-12-16 11:32:11  阅读:159  来源: 互联网

标签:HungrySingleton 模式 public instance static private 单例 浅析


懒汉单例

首先,写一个比较简单的懒汉模式的单例

public class SimpleSingleton {
    private static SimpleSingleton singleton;

    private SimpleSingleton() {
    }
    public static SimpleSingleton getInstance() {
        if (singleton == null) { // 1.判断是否为空
            singleton = new SimpleSingleton(); //2.进行初始化的操作
        }
        return singleton;
    }
}

懒汉的意思呢,就是我特别懒,我想吃东西的时候我才会去准备.

对应单例来说,就是每次使用都要尝试去创建单例对象。

以上的单例模式存在着一定的问题,首先都会想到的,就是多线程的问题。

因为1,2并不是原子性的操作

如果在多线程访问的情况下,其中一个线程执行到了第2步

在初始化成员变量时如果没有执行完,这个时候另外的线程 进行 第1步,判断成员变量依然为空,将执行instance 的 初始化,如此,会出现多个线程都进行初始化操作,从而获取不同的单例对象,就不符合单例的要求了。

既然原理了解后,可以进行下测试,借用IDEA的Debug 工具

将第一个线程Thread0 阻塞在 instance 初始化时,继续执行其他线程 

 最终的结果,则为 Thread0 和 其它线程获取的 对象并非同一个

如果存在多线程问题,我们想到的第一方法就是 加锁,如下所示

public static SimpleSingleton getInstance() {
        synchronized (SimpleSingleton.class) {
            if (singleton == null) { // 1.判断是否为空
                singleton = new SimpleSingleton(); //2.进行初始化的操作
            }
        }
        return singleton;
    }

这样第1步和第2步就变成了原子操作,但是也导致了多线程的串行化,对效率存在一定的影响,

因在此基础上又出现了一种解决方案,通过Double Checking Locking(DCL) 将 第1步前置,相当于instance初始化完成后的其他线程获取实例时并不会存在加锁和解锁的开销,毕竟锁还是比较影响性能的。

经过修改后,代码如下所示

public static SimpleSingleton getInstance() {
        if (null == singleton) {
            synchronized (SimpleSingleton.class) {
                if (singleton == null) { // 1.判断是否为空
                    singleton = new SimpleSingleton(); //2.进行初始化的操作
                }
            }
        }
        return singleton;
    }

但以上代码 依然会存在问题,这就要回到之前第2步 在底层执行的操作问题,分为三个操作:

(1)分配一块内存

(2)在内存上初始化成员变量

(3)将instance 引用指向内存

由于操作(2) 和 (3)之前会存在 重排序,即先将instance执向内存,在初始化成员变量。

此时,另外一个线程可能拿到一个未完全初始化的对象。

如果直接访问单例对象中的成员变量,就可能出错。

造成了一个典型的“构造函数溢出”问题。

解决方法也比较简单,使用volatile 对instance 进行修饰,代码如下:

private volatile static SimpleSingleton singleton;

    private SimpleSingleton() {
    }
    public static SimpleSingleton getInstance() {
        if (null == singleton) {
            synchronized (SimpleSingleton.class) {
                if (singleton == null) { // 1.判断是否为空
                    singleton = new SimpleSingleton(); //2.进行初始化的操作
                }
            }
        }
        return singleton;
    }

这样多线程安全的问题就解决了。至于 volatile 关键字的作用,这要讲起来内容可就多了,暂时只谈单例相关的/

当然懒汉的单例模式在实际项目中使用的并不是很多,下面分析一下饿汉的单例模式

饿汉单例

饿汉的意思就是我一直都是饿的,因此只好把食物都准备好,我想吃就吃;

public class HungrySingleton {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

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

看样子饿汉比较简单哈,示例可以参考 Java 中的Runtime 类,非常典型的饿汉模式

存在问题,序列化和反序列化问题

public class HungrySingleton implements Serializable {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singletonFile"));
        out.writeObject(instance);

        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("singletonFile"));
        HungrySingleton newInstance = (HungrySingleton) inputStream.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
    }
}

执行结果:instance 和 newInstance 并非同一个对象

 这就需要去仔细走读下反序列化的源码了,通过代码走读会发现 反序列化 通过了 反射的方法创建了对象,则造成了反序列化后的对象与序列化之前并不是同一个

具体代码走读,可参考链接 单例、序列化和readResolve()方法 - 掘金

解决方案的代码

public class HungrySingleton implements Serializable {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }   

    // 用于解决序列化和反序列化的问题
    public Object readResolve() {
        return instance;
    }
}

在反序列化时,会调用readResolve方法,通过反射获取原来的对象,对进行替换,这样,就可以保证序列化前后的对象是同一个了。

既然在反序列化过程中,出现了这个问题,那么在项目中也有可能有人会使用反射来创建该对象,如此就又会出现问题,

简单的解决办法就是,禁止该实例通过反射进行创建

public class HungrySingleton implements Serializable {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
        if (instance != null) {
            throw new RuntimeException("禁止通过反射创建");
        }
    }

    public static HungrySingleton getInstance() {
        return instance;
    }

    public Object readResolve() {
        return instance;
    }
}

当然,这个方法不能用于懒汉模式,因为懒汉模式是属于延迟创建,不能创建时进行抛错吧。

静态内联单例

饿汉单例还存在着延迟加载的问题,例如 instance 会随项目一同初始化,有可能在之后的项目运行中,很长时间都不会用到,但是却一直常驻内存,浪费资源 。

可以通过 Inner Class 的方法解决

public class StaticInnerClassSingleton {

    public static class InnerClass {
        static {
            System.out.println("inner class");
        }
        private static StaticInnerClassSingleton staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton() {}

    public static StaticInnerClassSingleton getInstance() {
        System.out.println("get instance");
        return InnerClass.staticInnerClassSingleton;
    }

    public static void main(String[] args) {
        StaticInnerClassSingleton.getInstance();
    }
}

这样就保证了当需要该对象时才会在内存中初始化。

容器单例

public class ContainerSingleton {

    private static HashMap map =  new HashMap<String, Object>();
    private ContainerSingleton() {}
    public static void putInstance(String key, Object o) {
        if (key != null && key.length() > 0 && o != null){
            map.put(key,o);
        }
    }

    public static Object getInstance(String key) {
        return map.get(key);
    }

}

直接上代码吧,这个也是项目中比较常见的,借用map的key-value结构,保证单例的唯一性。

具体的示例可参考 Spring 中的  SingletonBeanRegistry 接口

枚举单例

最简单,最安全的写法

public enum EnumSingleton {
    INSTANCE {
    };

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

 ThreadLocal 单例

和线程相关的单例模式

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal(){
        @Override
        protected Object initialValue () {

            return new ThreadLocalSingleton();
        }
    };

    private ThreadLocalSingleton()
    {}

    public static ThreadLocalSingleton getInstance(){
        return threadLocal.get();
    }
}

Mybatis 中的 ErrorContext使用了该单例模式

 不过要注意垃圾回收的问题

原文链接:单例模式有这么多种写法(JAVA单例模式浅析)

标签:HungrySingleton,模式,public,instance,static,private,单例,浅析
来源: https://blog.csdn.net/qq_31611039/article/details/121966652

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有