单例存在哪里问题?
1. 单例对 oop 的反对不敌对
OOP 四大个性: 封装 继承 多态 形象
而单例这种设计模式对于其中的形象 继承 多态 都反对的不好 为什么这么说呢?
咱们先来看一个单例的例子
public class Singleton_4 {
// 应用外部类形式结构单例, 线程平安并且懒加载
private AtomicInteger id = new AtomicInteger(0);
private Singleton_4() {}
public static Singleton_4 getInstance() {return SingletonCreator.singleton_4;}
private static class SingletonCreator {static Singleton_4 singleton_4 = new Singleton_4();
}
public Integer getIncrementId() {return this.id.getAndIncrement();
}
}
for (int i = 0; i < 100; i++) {
// 获取实例
Singleton_4 instance = Singleton_4.getInstance();
// 输入地址
System.out.println("实例的地址:" + instance);
// 获取 id
System.out.println(instance.getIncrementId());
System.out.println("------------------------------");
}
实例的地址:Singleton_4@63947c6b
0
-------------------------------------------------
实例的地址:Singleton_4@63947c6b
1
-------------------------------------------------
实例的地址:Singleton_4@63947c6b
2
-------------------------------------------------
实例的地址:Singleton_4@63947c6b
3
-------------------------------------------------
实例的地址:Singleton_4@63947c6b
4
-------------------------------------------------
这是因为 单例的应用形式违反了基于接口而非实现编程准则, 也就违反了狭义上了解的 OOP 的形象个性。如果将来某一天,咱们心愿针对不同的业务采纳不同的 ID 生成算法。比方,订单 ID 和用户 ID 采纳不同的 ID 生成器来生成。为了应答这个需要变动,咱们须要批改所有用到 IdGenerator 类的中央,这样代码的改变就会比拟大
除此之外,单例对继承、多态个性的反对也不敌对。这里我之所以会用“不敌对”这个词,而非“齐全不反对”,是因为从实践上来讲,单例类也能够被继承、也能够实现多态,只是实现起来会十分奇怪,会导致代码的可读性变差。不明确设计用意的人,看到这样的设计,会感觉莫名其妙。所以,一旦你抉择将某个类设计成到单例类,也就意味着放弃了继承和多态这两个强有力的面向对象个性,也就相当于损失了能够应答将来需要变动的扩展性。
2. 单例会暗藏类之间的依赖关系
代码的可读性十分重要。在浏览代码的时候,咱们心愿一眼就能看出类与类之间的依赖关系,搞清楚这个类依赖了哪些外部类。通过构造函数、参数传递等形式申明的类之间的依赖关系,咱们通过查看函数的定义,就能很容易辨认进去。然而,单例类不须要显示创立、不须要依赖参数传递,在函数中间接调用就能够了。如果代码比较复杂,这种调用关系就会十分荫蔽。在浏览代码的时候,咱们就须要认真查看每个函数的代码实现,能力晓得这个类到底依赖了哪些单例类。
3. 单例对代码的扩展性不敌对
在零碎设计初期,咱们感觉零碎中只应该有一个数据库连接池,这样能不便咱们管制对数据库连贯资源的耗费。所以,咱们把数据库连接池类设计成了单例类。但之后咱们发现,零碎中有些 SQL 语句运行得十分慢。这些 SQL 语句在执行的时候,长时间占用数据库连贯资源,导致其余 SQL 申请无奈响应。为了解决这个问题,咱们心愿将慢 SQL 与其余 SQL 隔离开来执行。为了实现这样的目标,咱们能够在零碎中创立两个数据库连接池,慢 SQL 独享一个数据库连接池,其余 SQL 独享另外一个数据库连接池,这样就能防止慢 SQL 影响到其余 SQL 的执行。如果咱们将数据库连接池设计成单例类,显然就无奈适应这样的需要变更,也就是说,单例类在某些状况下会影响代码的扩展性、灵活性。所以,数据库连接池、线程池这类的资源池,最好还是不要设计成单例类。实际上,一些开源的数据库连接池、线程池也的确没有设计成单例类。
4. 单例对代码的可测试性不敌对
单例模式的应用会影响到代码的可测试性。如果单例类依赖比拟重的内部资源,比方 DB,咱们在写单元测试的时候,心愿能通过 mock 的形式将它替换掉。而单例类这种硬编码式的应用形式,导致无奈实现 mock 替换
5. 单例不反对有参数的构造函数
单例不反对有参数的构造函数,比方咱们创立一个连接池的单例对象,咱们没法通过构造函数来指定连接池的大小. 上面有 两种解决方案
1. 应用特定的初始化办法
public class Singleton {
private static Singleton instance = null;
private final int paramA;
private final int paramB;
private Singleton(int paramA, int paramB) {
this.paramA = paramA;
this.paramB = paramB;
}
public static Singleton getInstance() {if (instance == null) {throw new RuntimeException("Run init() first.");
}
return instance;
}
public synchronized static Singleton init(int paramA, int paramB) {if (instance != null){throw new RuntimeException("Singleton has been created!");
}
instance = new Singleton(paramA, paramB);
return instance;
}
}
Singleton.init(10, 50); // 先 init,再应用
Singleton singleton = Singleton.getInstance();
2. 将参数放到 getIntance() 办法中
public class Singleton {
private static Singleton instance = null;
private final int paramA;
private final int paramB;
private Singleton(int paramA, int paramB) {
this.paramA = paramA;
this.paramB = paramB;
}
public synchronized static Singleton getInstance(int paramA, int paramB) {if (instance == null) {instance = new Singleton(paramA, paramB);
}
return instance;
}
}
Singleton singleton = Singleton.getInstance(10, 50);
不晓得你有没有发现,下面的代码实现略微有点问题。如果咱们如下两次执行 getInstance() 办法,那获取到的 singleton1 和 signleton2 的 paramA 和 paramB 都是 10 和 50。也就是说,第二次的参数(20,30)没有起作用,而构建的过程也没有给与提醒,这样就会误导用户
如果要解决这种问题. 既然是单例模式,类自身的初始化过程就只容许有一次,那么我倡议就不要在 getInstance 中做参数的传递,间接以配置文件的模式,办法外部间接读取配置参数,这样就不会误导用户了。
有什么代替计划
为了保障全局惟一,除了应用单例,咱们还能够用静态方法来实现。这也是我的项目开发中常常用到的一种实现思路。比方:
// 静态方法实现形式
public class IdGenerator {private static AtomicLong id = new AtomicLong(0);
public static long getId() {return id.incrementAndGet();
}
}
// 应用举例
long id = IdGenerator.getId();
不过,静态方法这种实现思路,并不能解决咱们之前提到的问题。实际上,它比单例更加不灵便,比方,它无奈反对提早加载。咱们再来看看有没有其余方法。实际上,单例除了咱们之前讲到的应用办法之外,还有另外一种应用办法。具体的代码如下所示:
// 1. 老的应用形式
public demofunction() {
//...
long id = IdGenerator.getInstance().getId();
//...
}
// 2. 新的应用形式:依赖注入
public demofunction(IdGenerator idGenerator) {long id = idGenerator.getId();
}
// 内部调用 demofunction()的时候,传入 idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);
基于新的应用形式,咱们将单例生成的对象,作为参数传递给函数(也能够通过构造函数传递给类的成员变量),能够解决单例暗藏类之间依赖关系的问题。不过,对于单例存在的其余问题,比方对 OOP 个性、扩展性、可测性不敌对等问题,还是无奈解决。所以,如果要齐全解决这些问题,咱们可能要从根上,寻找其余形式来实现全局惟一类。实际上,类对象的全局唯一性能够通过多种不同的形式来保障。咱们既能够通过单例模式来强制保障,也能够通过工厂模式、IOC 容器(比方 Spring IOC 容器)来保障,还能够通过程序员本人来保障(本人在编写代码的时候本人保障不要创立两个类对象)。这就相似 Java 中内存对象的开释由 JVM 来负责,而 C++ 中由程序员本人负责,情理是一样的。
深刻了解单例
如何了解单例模式中的唯一性?
咱们从新看一下单例的定义:“一个类只容许创立惟一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”定义中提到,“一个类只容许创立惟一一个对象”。那对象的唯一性的作用范畴是什么呢?是指线程内只容许创立一个对象,还是指过程内只容许创立一个对象?答案是后者,也就是说,单例模式创立的对象是过程惟一的。这里有点不好了解,我来具体地解释一下。咱们编写的代码,通过编译、链接,组织在一起,就形成了一个操作系统能够执行的文件,也就是咱们平时所说的“可执行文件”(比方 Windows 下的 exe 文件)。可执行文件实际上就是代码被翻译成操作系统可了解的一组指令,你齐全能够简略地了解为就是代码自身。当咱们应用命令行或者双击运行这个可执行文件的时候,操作系统会启动一个过程,将这个执行文件从磁盘加载到本人的过程地址空间(能够了解操作系统为过程调配的内存存储区,用来存储代码和数据)。接着,过程就一条一条地执行可执行文件中蕴含的代码。比方,当过程读到代码中的 User user = new User(); 这条语句的时候,它就在本人的地址空间中创立一个 user 长期变量和一个 User 对象。过程之间是不共享地址空间的,如果咱们在一个过程中创立另外一个过程(比方,代码中有一个 fork() 语句,过程执行到这条语句的时候会创立一个新的过程),操作系统会给新过程调配新的地址空间,并且将老过程地址空间的所有内容,从新拷贝一份到新过程的地址空间中,这些内容包含代码、数据(比方 user 长期变量、User 对象)。
所以,单例类在老过程中存在且只能存在一个对象,在新过程中也会存在且只能存在一个对象。而且,这两个对象并不是同一个对象,这也就说,单例类中对象的唯一性的作用范畴是过程内的,在过程间是不惟一的。
如何实现线程惟一的单例?
线程惟一单例的代码实现很简略,如下所示。在代码中,咱们通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样咱们就能够做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。在 JAVA 中 线程实现单例 必定会有同学想到 ThreadLocal 实际上 ThreadLocal 工具类,能够更加轻松地实现线程惟一单例。不过,ThreadLocal 底层实现原理也是基于上面代码中所示的 HashMap。
public class IdGenerator {private AtomicLong id = new AtomicLong(0);
private static final ConcurrentHashMap<Long, IdGenerator> instances
= new ConcurrentHashMap<>();
private IdGenerator() {}
public static IdGenerator getInstance() {Long currentThreadId = Thread.currentThread().getId();
instances.putIfAbsent(currentThreadId, new IdGenerator());
return instances.get(currentThreadId);
}
public long getId() {return id.incrementAndGet();
}
}
如何实现集群环境下的单例?
集群相当于多个过程形成的一个汇合,“集群惟一”就相当于是过程内惟一、过程间也惟一。也就是说,不同的过程间共享同一个对象,不能创立同一个类的多个对象。
如果严格依照不同的过程间共享同一个对象来实现,那集群惟一的单例实现起来就有点难度了。具体来说,咱们须要把这个单例对象序列化并存储到内部共享存储区(比方文件)。过程在应用这个单例对象的时候,须要先从内部共享存储区中将它读取到内存,并反序列化成对象,而后再应用,应用实现之后还须要再存储回内部共享存储区。为了保障任何时刻,在过程间都只有一份对象存在,一个过程在获取到对象之后,须要对对象加锁,防止其余过程再将其获取。在过程应用完这个对象之后,还须要显式地将对象从内存中删除,并且开释对对象的加锁。
public class IdGenerator {private AtomicLong id = new AtomicLong(0);
private static IdGenerator instance;
private static SharedObjectStorage storage = FileSharedObjectStorage(/* 入参省略,比方文件地址, 或者这里能够应用 redis 之类的 */);
private static DistributedLock lock = new DistributedLock();
private IdGenerator() {}
public synchronized static IdGenerator getInstance()
if (instance == null) {lock.lock();
instance = storage.load(IdGenerator.class);
}
return instance;
}
public synchroinzed void freeInstance() {storage.save(this, IdGeneator.class);
instance = null; // 开释对象
lock.unlock();}
public long getId() {return id.incrementAndGet();
}
}
// IdGenerator 应用举例
IdGenerator idGeneator = IdGenerator.getInstance();
long id = idGenerator.getId();
IdGenerator.freeInstance();
结:
在文章中,咱们讲到单例唯一性的作用范畴是过程,实际上,对于 Java 语言来说,单例类对象的唯一性的作用范畴并非过程,而是类加载器(Class Loader)
要答复这个问题,要了解 classloader 和 JDK8 中应用的双亲委派模型。
classloader 有两个作用:1. 用于将 class 文件加载到 JVM 中;2. 确认每个类应该由哪个类加载器加载,并且也用于判断 JVM 运行时的两个类是否相等。
双亲委派模型的原理是当一个类加载器接管到类加载申请时,首先会申请其父类加载器加载,每一层都是如此,当父类加载器无奈找到这个类时(依据类的全限定名称),子类加载器才会尝试本人去加载。
所以双亲委派模型解决了类反复加载的问题,比方能够试想没有双亲委派模型时,如果用户本人写了一个全限定名为 java.lang.Object 的类,并用本人的类加载器去加载,同时 BootstrapClassLoader 加载了 rt.jar 包中的 JDK 自身的 java.lang.Object,这样内存中就存在两份 Object 类了,此时就会呈现很多问题,例如依据全限定名无奈定位到具体的类。有了双亲委派模型后,所有的类加载操作都会优先委派给父类加载器,这样一来,即便用户自定义了一个 java.lang.Object,但因为 BootstrapClassLoader 曾经检测到本人加载了这个类,用户自定义的类加载器就不会再反复加载了。所以,双亲委派模型可能保障类在内存中的唯一性。
分割到课后的问题,所以用户定义了单例类,这样 JDK 应用双亲委派模型加载一次之后就不会反复加载了,保障了单例类的过程内的唯一性,也能够认为是 classloader 内的唯一性。当然,如果没有双亲委派模型,那么多个 classloader 就会有多个实例,无奈保障唯一性。
启动类加载器: 加载 JAVA_HOME\lib 目录下的类库
↑
扩大类加载器: 加载 JAVA_HOME\lib\ext 目录下的类库, 是 java SE 扩大性能, jdk9 被模块化的人造扩大能力所取代
↑
应用程序加载器: 加载用户的应用程序
↑
用户自定义的加载器: 供用户扩大应用, 加载用户想要的内容
这个类加载器的档次关系被称为类的 ” 双亲委派模型 ”
文中的我的项目 github 地址:link