相关阅读:
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();

//使用缓存
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.