预计阅读时间:10 - 13 min
「南媒婆北媒婆,南北媒婆送老婆」
建建:很是羡慕宫廷剧中的后宫佳丽三千啊,可惜这个社会不会再出现这样的现象了。
子乾:可拉倒吧,你这智商是不是在治伤的过程中给致伤了。要是这样,她们宁愿当易烊千玺第 1001个 老婆,都不愿意做你老婆。
(媒婆来了)
南媒婆:建建快出来,今天我高兴,来给你说个老婆。(领来一花姑娘)
建建:哦,?,喔,谢谢阿婆。我们处下试试。
(第二天)
北媒婆:建建出来了,今天姨高兴,给你领来一老婆。(又一花姑娘)
建建:诶诶,情形不对啊。昨天一个,今天一个,这碰到一块不干仗嘛?子乾快出来,给我顶一下。
子乾:难道你要成了下一个易烊千玺,老婆天天送上门?.....
只能有一个老婆,只能有一个对象。媒婆一会创建一个对象,一会领来一个对象,违反了常理。当领来对象时,应该先看下家里是否有对象了,没有,可以领;有,则不能领!这是规矩。
唯一,仅一个对象。映射到代码里面就是该类在运行过程中只能有一个对象,别让人瞎创建,容易出事。解决这个问题,单例模式就来了。
来了来了,他来了。他来阻止你造对象来了。
Singleton Pattern:Ensure a class has only one instance, and provide a global point of access to it.
没读懂,看下面:
单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
要点:
▎某个类,可以没有实例,有的话,只能有一个。
▎必须自己创建这个实例。(不允许在其他类中创建实例)
▎必须主动向整个系统提供这个实例。
它是对象创建型模式。
单例模式类图:
角色:
Singleton 单例
代码解读:
public class Singleton{
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null)
instance = new Singleton();
return instance;
}
}
·私有静态单例对象
·私有构造方法
·公有静态方法,返回值为单例类型
a. 只可以有一个实例,将 instance 对象声明为 private、static 。并且构造方法设置为 private,其他类中无法调用,就无法生成该类对象。
其他类中无法调用该类方法,怎么来的该类对象?没有该类对象,怎么调用该类中的方法?这就是令人头疼的“鸡生蛋,蛋生鸡”的问题。
好在,static 静态方法,可以由类名直接调用!
b. 在 getInstance 方法中,进行判断实例是否为 null,等于 null 时才创建。不为 null,直接返回现有的 instance 对象。
单例应用:
来看一个实际的应用场景。
百度智能平台提供的多种智能 API 接口,在调用过程中,都会涉及到 access_token。它的有效期为 30 天。
因此一旦有了该 token,后面不需要每次使用 API 时都产生一个。
但官方给的 sample 中,如果直接使用,则是每次都产生一个 token,实属浪费。人家也给提示了,建议单例使用。
比如从图片中提取文字,AipOcr :
官网的 Ocr 样例代码:
public class Sample {
//设置APPID/AK/SK
public static final String APP_ID = "你的 App ID";
public static final String API_KEY = "你的 Api Key";
public static final String SECRET_KEY = "你的 Secret Key";
public static void main(String[] args) {
// 初始化一个AipOcr
AipOcr client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);
// 可选:设置网络连接参数
client.setConnectionTimeoutInMillis(2000);
client.setSocketTimeoutInMillis(60000);
// 可选:设置代理服务器地址, http和socket二选一,或者均不设置
client.setHttpProxy("proxy_host", proxy_port); // 设置http代理
client.setSocketProxy("proxy_host", proxy_port); // 设置socket代理
// 可选:设置log4j日志输出格式,若不设置,则使用默认配置
// 也可以直接通过jvm启动参数设置此环境变量
System.setProperty("aip.log4j.conf", "path/to/your/log4j.properties");
// 调用接口
String path = "test.jpg";
JSONObject res = client.basicGeneral(path, new HashMap<String, String>());
System.out.println(res.toString(2));
}
}
AipOcr 在 package com.baidu.aip.ocr 包中,是个很成熟的类。
我们考虑通过继承来接盘 AipOcr 所具备的功能,并且不影响其他地方的使用。
类图:
AipOcrSubSingleton 是核心,就是我们想要的单例类。它继承了 AipOcr,该类有私有的静态的实例化对象,私有的构造方法,公有的获取实例化对象的方法。
客户端中,对此进行调用,获取实例化对象并执行其他功能。
代码:
AipOcrSubSingleton 类,核心三个要点一个不能少。
AipOcr 记得先在pom中引入依赖或者下载jar包引入进项目。
package singleton;
import com.baidu.aip.ocr.AipOcr;
public class AipOcrSubSingleton extends AipOcr {
private static AipOcrSubSingleton clientSingleton;
private AipOcrSubSingleton(String appId, String apiKey, String secretKey) {
super(appId, apiKey, secretKey);
}
public static AipOcrSubSingleton getClientSingleton(String appId, String apiKey, String secretKey){
if(clientSingleton == null){
clientSingleton = new AipOcrSubSingleton(appId, apiKey, secretKey);
}
return clientSingleton;
}
}
client 类(根据官方sample 类修改):
在这里,由于是一个普通的 Java 项目,没办法实现多人多机器访问。因此,我写了一个 “第二次”调用执行的代码。
以此来验证,如果第一次识别图像 1 的access_token 和识别图像 2 的access_token 一样,证明第二次执行时,没有创建新单例对象。
package singleton;
import java.util.HashMap;
import org.json.JSONObject;
public class Client {
//设置APPID/AK/SK
public static final String APP_ID = "17370504";
public static final String API_KEY = "LpZTq7KZDnDtb93Ydor1aMC7";
public static final String SECRET_KEY = "17umB4mByrlZ3S2SXupwmavtL8pW7Xtq";
public static void main(String[] args) {
// 单例调用
AipOcrSubSingleton client = AipOcrSubSingleton.getClientSingleton(APP_ID, API_KEY, SECRET_KEY);
// 可选:设置网络连接参数
client.setConnectionTimeoutInMillis(2000);
client.setSocketTimeoutInMillis(60000);
// 调用接口
String path = "/Users/zfj/Desktop/1.png";
JSONObject res = client.basicGeneral(path, new HashMap<String, String>());
System.out.println(res.toString(2));
//第二次
client = AipOcrSubSingleton.getClientSingleton(APP_ID, API_KEY, SECRET_KEY);
String path2 = "/Users/zfj/Desktop/2.png";
JSONObject res2 = client.basicGeneral(path2, new HashMap<String, String>());
System.out.println(res2.toString(2));
}
}
通过 debug 跟踪,两次单例对象,access_token 一样:
singleton.AipOcrSubSingleton@2ac1fdc4
access_token:(被修改,不可直接用)
24.43b64052096zfjnb723874c92b41e5a18b.zfjnb.1588237981.282335-17370504
识别结果:
项目结构:
一个包下两个类,完事。
两张原图:
程序中的 1.png
程序中的 2.png
单例模式优点:
· 提供了对唯一实例的受控访问。严格控制如何、何时访问它;
· 节约系统资源。
单例模式缺点:
· 没有抽象层,不利于扩展;
· 单例类职责过重,违背了“单一职责原则”;
· 垃圾收集器可能吃掉单例对象(Java 1.2 之前可能会,Java 1.2 之后无须担心)
即我们现在几乎不用担心。
适用环境:
· 系统只需要一个实例对象,比如电脑的注册表设置、线程池、缓存的对象,日志对象,充当打印机驱动程序的对象等等。
· 客户调用累的单个实例只允许使用一个公共访问点。
单例模式拓展:
单例模式中很大的一个坑,就是多线程问题。
当该实例对象未生成,有两个或多个线程同时访问时,可能造成单例对象不同步问题。一旦有了实例对象,就不存在该问题了。如何解决?
别怕,只需要把 getInstance() 编程同步方法即可:synchronized
public static synchronized Singleton getInstance(){}
(它的矛盾点出在创建单例对象冲突上)
但每次都同步一下,有点浪费时间。而且只是在第一次执行就可以,后面(有了实例对象)无需再执行,有它还成了累赘了。
那就通过 饿汉式单例 来解决。
饿汉式单例:
在静态初始化器中创建单例,getInstance() 直接返回单例对象。就保证了线程安全。
public class Singleton{
private static Singleton instance = new Singleton;
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
有 饿汉 就有 懒汉,之前的就是懒汉式,在第一次需要时才创建单例对象;一直没用到,一直就不会创建。
饿汉 VS 懒汉
☉饿汉式单例类在自己被加载是就将自己实例化,比懒汉式资源利用效率低;
☉饿汉式单例速度和响应时间由于懒汉式,不需要每次判断。
☉懒汉式必须在多个线程同时首次引用单例类时的访问限制问题,否则可能出错。
有没有其他方法呢?
有。双重检查锁(double-checked locking)。
首先检查实例是否已经被创建,如果未被创建,则进行同步。只有第一次会同步,其他时候直接使用单例对象。
public class Singleton{
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance(){
if (instance == null){
synchronized(Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
关键字:volatile 、synchronized()方法。
volatile 确保当 instance 变量被初始化成 Singleton 实例时,多个线程正确处理 instance 变量。
getInstance() 中的同步动作,只有在 instance 未初始化时进行。一旦初始化,第一个 if 语句内容不需要执行。
注意:双重检查锁只适用于 1.4 之后的版本(不含 1.4)。
扩展思考:
借助单例类,约束其他的类。在单例类的方法中约束创建其他类的对象。
比如调用百度智能 API 时,在一个 BaseSingleton 单例类(自己创建)中,控制只生成一个 AipOcr (百度提供)对象。则不用继承 AipOcr。
这种实现不是严格的单例模式,不是该单例类的实例,但从功能上,却满足了一定要求。
这个有待商榷,期待和你交流。
致谢:
最后,亲爱的读者朋友,坚持看完不容易,篇幅长,尽量分享仔细全面。如有疑惑,欢迎与我交流。
感谢阅读,感谢陪伴。
表情包来源于网络,侵删。
「子乾建建为作者笔名」