这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战
1. 广告邮件推送
现在大部分系统都有「发送邮件」的功能,系统可以通过邮件的方式给客户发送一些通知、消息提醒、广告推送的邮件。 其中,「广告邮件」比较特殊,它的邮件内容都是一样的,唯一不同的目标邮箱,而且它需要短时间大批量的发送,对发送的性能要求比较高。
我们试着用Java代码来描述邮件类,如下:
@Setter
public class Mail{
private String target;//目标邮箱
private String content;//邮件内容
private String tail;//公司落款
// 发送
public void send(){
System.out.println("target:" + target + ",content:" + content);
}
}
客户端调用:
Mail mail = new Mail();
// 广告的内容都是一样的,一般都是从数据库查询获得
mail.setContent("AirPods Pro发布,主动降噪,声声入耳更沉浸。");
// 落款也都是一样的
mail.setTail("Apple");
// 批量发送
for (int i = 0; i < 1000; i++) {
mail.setTarget(i + "@qq.com");
mail.send();
}
输出:
target:995@qq.com,content:AirPods Pro发布,主动降噪,声声入耳更沉浸。
......
功能是实现了,但是有一个问题,邮件的发送是单线程的,假设一封邮件发送的时间是50ms,发送一百万封邮件需要将近14个小时,这显然是不能接收的,必须采用多线程并行发送。
// 开启16个线程并发推送
for (int i = 0; i < 16; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
mail.setTarget(j + "@qq.com");
mail.send();
}
}).start();
}
修改客户端代码如上,运行,很快你就会发现,有的用户没受到邮件,有的用户却收到多封邮件了,这是怎么回事?mail对象线程不安全了,target是共享变量,多个线程在修改它。
再修改,线程内创建Mail对象:
new Thread(()->{
// 线程内创建对象,方法栈私有,安全。
Mail mail = new Mail();
// 广告的内容都是一样的,一般都是从数据库查询获得
mail.setContent("AirPods Pro发布,主动降噪,声声入耳更沉浸。");
// 落款也都是一样的
mail.setTail("Apple");
for (int j = 0; j < 1000; j++) {
mail.setTarget(j + "@qq.com");
mail.send();
}
}).start();
这下终于线程安全了,但是仔细分析一下程序,邮件的内容和落款都是一样的,为什么要重复执行一百万次呢?为什么每次都要生成一个空对象呢?能不能从一个已有的对象中直接克隆一份,然后只设置一些个性化的属性呢?
可以,当然可以了,原型模式应运而生。
JDK的Object提供了一个clone()方法:
protected native Object clone() throws CloneNotSupportedException;
它是被保护的本地方法,clone()方法不能直接调用,需要类实现java.lang.Cloneable接口,Cloneable是一个标记接口,实现它代表类是允许被克隆的。
实现Cloneable接口后,还需要类重写clone()方法,Mail类修改如下:
@Setter
public class Mail implements Cloneable{
// 省略部分代码
@Override
protected Mail clone(){
return (Mail) super.clone();
}
}
OK,现在客户端可以非常方便的调用clone()方法来获得一个Mail对象:
Mail mail = new Mail();
mail.setContent("AirPods Pro发布,主动降噪,声声入耳更沉浸。");
mail.setTail("Apple");
Mail clone = mail.clone();
clone.setTarget(j + "@qq.com");
clone.send();
clone()是从一个已有的对象中克隆出来的新对象,因此不存在线程安全问题,且会保留已有对象的所有属性,而且因为是基于内存的直接拷贝,构造函数不会执行,性能上也会比使用new关键字直接创建会好一些。
简单吧,这就是原型模式!
2. 原型模式的定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式的核心就是一个clone()方法,它允许客户端无需生成一个新的空对象,而是从一个已有的对象中克隆出一个一模一样的新对象,只需设置一些自定义的属性即可快速使用。
3. 原型模式的优点
- 性能很好,Java的
clone()是基于内存二进制流的拷贝,性能要比new创建对象好。 - 避免构造函数的约束,
clone()产生的对象不会执行构造函数。 - 避免对象的复杂构建过程,
clone()可以基于一个已有的复杂对象进行克隆,避免了复杂的构建过程。
4. 原型模式的使用场景
- 当一个类的构建需要消耗大量资源时(如查询数据库,读取文件等),使用原型模式可以进行资源优化。
- 同一个对象会有多个修改者时,使用原型模式构建新对象,避免数据错乱。
- 需要优化创建对象的性能。
5. 原型模式的扩展
5.1 构造函数不会执行
调用clone()创建对象时,类的构造函数是不会被执行的,这一点经常被Java开发者忽略。
原因是调用clone()方法时,JVM会重新分配一块内存,然后将原有对象的内存二进制流直接拷贝到新的内存区域,因此构造函数没有被执行也是可以说得通的。
5.2 浅拷贝和深拷贝
JDK的clone()是浅拷贝实现,对于引用类型,拷贝的只是对象引用的内存地址,对象A修改引用对象的属性后,对象B也会受到影响,这一点需要特别注意。
如果要实现深拷贝,需要开发人员自己去实现,一般做法是将依赖的引用类也挨个重写clone()方法,依次调用引用对象的clone()方法来拷贝引用类型对象并赋值给外层对象。
6. 总结
原型模式是23种设计模式中最简单的模式之一,它的简单程度和单例模式有的一拼。核心是类实现的clone()方法,它允许对象基于自身克隆出一个一模一样的新对象,JDK本身就支持原型模式,因此Java开发者可以直接拿来使用。