设计模式之单例模式

单例模式

Posted by Simon on October 15, 2018

单例模式保证系统中一个类只有一个实例。

目的及缺点(使用场景)

① 避免对象频繁创建销毁的性能开销,较少内存开支。(对象的使用频率较高)
② 单例能提供全局访问入口,共享资源访问。
③ 避免多重占用,单例能避免多个实例对同一资源的同时读写操作。
【缺点】 扩展难,只能通过修改代码实现需求变动;内存泄漏;单例类不要序列化。

代码实现方式

实现关键点:
1.构造函数私有化
2.提供public static方法获取实例
3.通过静态方法获取或创建实例时,确保在多线程环境下生成有且仅有一个实例(即确保线程安全)
4.确保反序列化时不会重新创建对象

① 懒汉式

public class User {

    private static User instance ;

    private User(){

    }

    public static User getUser(){
        if (instance == null) {
            instance = new User();
        }
        return instance;
    }

}

该写法非线程安全,在多线程情况下不能正常工作,可增加synchronized修饰getUser方法, 但同步方法会影响代码的执行效率,增加同步开销,且第一次加载稍慢。

② 饿汉式

public class User {

    private static User instance = new User() ;

    private User(){

    }

    public static User getUser(){
        return instance;
    }

}

该写法在创建类的同时会实例化好一个静态对象,线程安全,但达不到延迟初始化的效果。

③ 双重检查模式(DCL)

public class User {

    private static User instance;

    private User() {

    }

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

}

此写法能避免同步开销,且JDK1.5后能确保线程安全,第一次加载稍慢。
JDK1.5之前的包 在多线程环境下可能会出现DCL失效问题,1.5版本后,由于调整了JVM,可在变量声明前增加volatile关键字, 确保每次读取对象都从主内存读取,以牺牲点性能来确保程序的正确性。

④ 静态内部类方式

public class User {

    private User(){

    }

    public static User getUser() {
       return SingletonHolder.instance;
    }

    private static class SingletonHolder {
       private static User instance = new User();
    }

}

能避免线程安全问题,且调用getUser方法才会初始化实例。

⑤ 枚举
以上4种方式在反序列化的情况下会重新创建实例,枚举则不会。

public enum EnumUser {
    
    INSTANCE;

}

枚举写法简单,默认只有唯一一个实例,且线程安全。

⑥ 使用容器

public class User {

    private static Map<String,User> map = new HashMap<>();

    public static void registerUser(String key,User user){
        if (!map.containsKey(key)){
            map.put(key,user);
        }
    }

    public static User getUser(String key){
        return map.get(key);
    }

}

如何选择写法

选择哪种实现方式,取决于项目本身,如多线程并发情况是否复杂/JDK版本是否过低/性能要求等等。