Java编程思想(六)单例模式用途

2,160 阅读6分钟

banner窄.png

铿然架构  |  作者  /  铿然一叶 这是铿然架构的第 78 篇原创文章

相关阅读:

JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?
Java编程思想(五)事件通知模式解耦过程
Java编程思想(七)使用组合和继承的场景
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比
JAVA基础(五)函数式接口-复用,解耦之利刃
HikariPool源码(二)设计思想借鉴
【极客源码】JetCache源码(一)开篇
【极客源码】JetCache源码(二)顶层视图
人在职场(一)IT大厂生存法则


1. 单例模式的特点和用途

单例模式在同一个进程内只有一个实例,不会多次实例化。

由于在同一进程内只有一个实例,不会多次实例化,因此单例模式可以用来缓存数据和在进程内共享数据。

基于这两个特点,单例模式还可以用于在模块间解耦。

2. 单例模式的写法

单例模式的写法有很多种,对于应用开发者来说,记住其中一种就可以。(每个人的精力有限,要考虑投入产出比,所有方式都记住并没有太大必要。)

// 最终类,避免被继承(非必须,通常即使不用final修饰,也不会有人去继承一个单例类)
public final class SingletonDemo {
    // 静态实例变量;volatile关键字修饰,保证跨线程可见
    private static volatile SingletonDemo singletonDemo = null;

    // 私有构造器,避免外部直接实例化
    private SingletonDemo() {}

    // 静态方法获取实例
    public static SingletonDemo getInstance() {
        // 先做一次判断,不为空则返回
        if (singletonDemo == null) {
            // 因为是静态实例,进程可见,需要在class上加锁(进程级加锁)
            synchronized (SingletonDemo.class) {
                // 双重检查,在等待SingletonDemo.class锁的时候,另外一个线程可能已经做了初始化
                if (singletonDemo == null) {
                    singletonDemo = new SingletonDemo();
                }
            }
        }
        return singletonDemo;
    }
}

3. 用于缓存数据

在系统中有很多静态数据(不常变化的数据)可以放到缓存中,在使用时通过缓存访问,而不是每次都访问数据库,例如城市信息,证件类型,行业等等,这些都可以通过单例模式来实现。

public final class CertificateTypeMgr {
    private static CertificateTypeMgr certificateTypeMgr;

    private Map<String, CertificateTypeDTO> certificateTypeMap;

    private CertificateTypeMgr() {
        init();
    }

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

    public CertificateTypeDTO getCertificateType(String code) {
        System.out.println("getCertificateType");
        return certificateTypeMap.get(code);
    }

    private void init() {
        System.out.println("CertificateTypeMgr init....");
        certificateTypeMap = new ConcurrentHashMap<>();
        // 从数据库中获取数据初始化....
        certificateTypeMap.put("101", new CertificateTypeDTO("101", "ID Card"));
        certificateTypeMap.put("102", new CertificateTypeDTO("102", "Household Register"));
        certificateTypeMap.put("102", new CertificateTypeDTO("103", "Student Card"));
        System.out.println("CertificateTypeMgr init end.");
    }
}

public class CertificateTypeDTO {
    // m开头表示成员变量
    public String mCode;
    public String mName;

    public CertificateTypeDTO(String code, String name) {
        this.mCode = code;
        this.mName = name;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("CertificateTypeDTO {").append("code=").append(mCode).append(", ")
                .append("name=").append(mName).append("}");
        return builder.toString();
    }
}

public class EntryDemo {
    public static void main(String[] args) {
        // 通常缓存的初始化会在应用启动的时候初始化(勤快加载), 模拟启动加载,在不同的线程中
        new Thread() {
            @Override
            public void run() {
                CertificateTypeMgr.getInstance();
            }
        }.start();

        //使用缓存
        CertificateTypeDTO certificateTypeDTO = CertificateTypeMgr.getInstance().getCertificateType("102");
        System.out.println(certificateTypeDTO.toString());
    }
}

输出:

CertificateTypeMgr init....
CertificateTypeMgr init end.
getCertificateType, code=102
CertificateTypeDTO {code=103, name=Student Card}

从上面的访问方式可以看到,其本质是:通过要给静态对象实例(certificateTypeMgr)持有非静态成员变量(certificateTypeMap)实现了进程内数据共享。

那么不一定要用单例模式,直接通过静态方法访问静态变量也可以啊,尝试重构如下:

public class StaticCertificateTypeMgr {

    // 静态方法只能访问静态变量,需要static修饰
    private static Map<String, CertificateTypeDTO> certificateTypeMap;

    private static volatile boolean isInited;

    public static CertificateTypeDTO getCertificateType(String code) {
        System.out.println("getCertificateType, code=" + code);
        // 需要在初始化完成后才能返回,否则会抛出空指针异常
        if (isInited) {
            return certificateTypeMap.get(code);
        }
        return null;
    }

    public static void init() {
        System.out.println("CertificateTypeMgr init....");
        certificateTypeMap = new ConcurrentHashMap<>();
        // 从数据库中获取数据初始化....
        certificateTypeMap.put("101", new CertificateTypeDTO("101", "ID Card"));
        certificateTypeMap.put("102", new CertificateTypeDTO("102", "Household Register"));
        certificateTypeMap.put("102", new CertificateTypeDTO("103", "Student Card"));
        System.out.println("CertificateTypeMgr init end.");
        isInited = true;
    }
}

public class StaticEntryDemo {
    public static void main(String[] args) {
        // 通常缓存的初始化会在应用启动的时候初始化(勤快加载), 模拟启动加载,在不同的线程中
        new Thread() {
            @Override
            public void run() {
                StaticCertificateTypeMgr.init();
            }
        }.start();

![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/7/1728ef7878cf0a8e~tplv-t2oaga2asx-image.image)
        //使用缓存
        CertificateTypeDTO certificateTypeDTO = StaticCertificateTypeMgr.getCertificateType("102");
        System.out.println(certificateTypeDTO);
    }
}

输出:

getCertificateType, code=102
CertificateTypeMgr init....
null
CertificateTypeMgr init end.

可以看到,直接通过静态方法的方式,在获取缓存数据时不会等待缓存初始化完成,有可能获取不到数据,而单例模式能保证在获取数据时缓存已经初始化完成,可以获取到数据。

因此,虽然单例模式的本质也是通过静态实例实现进程内数据共享,但相比直接使用静态方法来获取静态变量数据,可用性更好。

4. 用于进程内数据共享

进程内数据共享的例子有用户的会话信息共享,比如登录用ID,IP地址等等,要在每次服务调用前用于鉴权,鉴权通过后才能调用服务。

其用法跟缓存的用法类似,不再描述。

5. 用于模块间解耦

模块间的关系应为单向依赖,避免双向依赖,如果出现了双向依赖,应把其中一个依赖拆出来形成公共的模块,如下:

重构前:

重构后:

而模块间解耦除了这种方式以外,在上一章中的事件通知模式也可以用于模块间解耦:

如图所示:

Listener在派发Event时,如果直接调用SubscriberImpl,那么就会导致模块A和模块B相互依赖,而通过EventRegistry这个单例对象就可以避免双向依赖,其调用过程如下:

1.模块B通过EventRegistry注册SubscriberImpl

2.Listener通过EventRegistry遍历所有Subscriber,然后通过Dispatcher来派发事件。

这样模块A就不会依赖模块B,避免双向依赖。

6. 总结

1. 单例模式可用于在进程内缓存和共享数据。
2. 在事件通知模式中,单例模式可用于模块间解耦。

end.


<--阅过留痕,左边点赞!