享元模式
享元模式(Flyweight Pattern):使用共享对象可有效地支持大量的细粒度的对象。
享元模式是池技术的重要实现方式。
要求细粒度对象,那么不可避免地使得对象数量多且性质相近,那我们就将这些对象的信息分为两个部分:内部状态(intrinsic)与外部状态(extrinsic)。
- 内部状态:对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变。
- 外部状态:对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态。
享元模式角色名称:
- Flyweight抽象享元角色:简单地说就是一个产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现。
- ConcreteFlyweight具体享元角色:具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。
- unsharedConcreteFlyweight不可共享的享元角色:不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中。
- FlyweightFactory享元工厂:职责非常简单,就是构造一个池容器,同时提供从池中获得对象的方法。
抽象享元角色Flyweight:
public abstract class Flyweight {
//内部状态
private String intrinsic;
//外部状态
protected final String Extrinsic;
//要求享元角色必须接受外部状态
public Flyweight(String _Extrinsic){
this.Extrinsic = _Extrinsic;
}
//定义业务操作
public abstract void operate();
//内部状态的getter/setter
public String getIntrinsic() {
return intrinsic;
}
public void setIntrinsic(String intrinsic) {
this.intrinsic = intrinsic;
}
}
具体享元角色ConcreteFlyweight1~2:
public class ConcreteFlyweight1 extends Flyweight{
//接受外部状态
public ConcreteFlyweight1(String _Extrinsic){
super(_Extrinsic);
}
//根据外部状态进行逻辑处理
public void operate(){
//业务逻辑
}
}
public class ConcreteFlyweight2 extends Flyweight{
//接受外部状态
public ConcreteFlyweight2(String _Extrinsic){
super(_Extrinsic);
}
//根据外部状态进行逻辑处理
public void operate(){
//业务逻辑
}
}
注意:在程序开发中,确认只需要一次赋值的属性则设置为final类型,避免无意修改导致逻辑混乱,特别是Session级的常量或变量。
享元工厂FlyweightFactory:
public class FlyweightFactory {
//定义一个池容器
private static HashMap<String,Flyweight> pool= new HashMap<String,Flyweight>();
//享元工厂
public static Flyweight getFlyweight(String Extrinsic){
//需要返回的对象
Flyweight flyweight = null;
//在池中没有该对象
if(pool.containsKey(Extrinsic)){
flyweight = pool.get(Extrinsic);
}else{
//根据外部状态创建享元对象
flyweight = new ConcreteFlyweight1(Extrinsic);
//放置到池中
pool.put(Extrinsic, flyweight);
}
return flyweight;}
}
享元模式的优点:
大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能。
享元模式的缺点:
提高了系统复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。
享元模式的使用场景:
- 系统中存在大量的相似对象。
- 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
- 需要缓冲池的场景
享元模式的扩展
- 线程安全的问题
- 性能平衡
享元模式的实例
由于已有的程序存在内存溢出,而内存溢出对Java应用来说实在是太平常了,有以下两种可能:
- 内存泄漏:无意识的代码缺陷,导致内存泄漏,JVM不能获得连续的内存空间。
- 对象太多:产生的对象太多,内存被耗尽。
这里我们使用对象池技术减少对象数量,对象池(Object Pool)的实现有很多开源工具,比如Apache的commons-pool 就是一个非常不错的池工具,我们暂时还用不到这种重量级的工具,我们自己来设计一个共享对象池,需要实现如下两个功能:
- 容器定义:定义一个池容器,在这个容器中容纳哪些对象。
- 提供客户端访问的接口:提供一个接口供客户端访问,池中有可用对象时,可以直接从池中获得,否则建立一个新的对象,并放置到池中。
原类图:
(1)新类图:
增加了一个子类,实现带缓冲池的对象建立,同时在工厂类上增加了一个容器对象HashMap,保存池中的所有对象。
注意:在对象池中,对象一旦产生,必然有一个唯一的、可访问的状态标志该对象,而且池中的对象声明周期是由池容器决定,而不是由使用者决定的。
public class SignInfo {
//报考人员ID
private String id;
//考试地点
private String location;
//考试科目
private String subject;
//邮寄地址
private String postAddress;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getPostAddress() {
return postAddress;
}
public void setPostAddress(String postAddress) {
this.postAddress = postAddress;
}
}
public class SignInfo4Pool extends SignInfo {
//定义一个对象池提取的key值
private String key;
//构造函数获得相同标志
public SignInfo4Pool(String key) {
this.key = key;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
import java.util.HashMap;
public class SignInfoFactory {
//池容器
private static HashMap<String, SignInfo> pool = new HashMap<String,SignInfo>();
//报名信息的对象工厂
@Deprecated
public static SignInfo SignInfo() {
return new SignInfo();
}
//从池中获得对象
public static SignInfo getSignInfo(String key) {
SignInfo result = null;
if (!pool.containsKey(key)) {
System.out.println(key + "-----建立对象,放到池中");
result = new SignInfo4Pool(key);
pool.put(key, result);
} else {
result = pool.get(key);
System.out.println(key + "-----直接从池中取得");
}
return result;
}
}
给不用的代码添加@Deprecated注解,不要有删除投产中代码的念头,如果方法或类确实不再使用了,增加该注解,表示该方法或类已经过时,尽量不要再使用了,我们应该保持历史原貌,同时也有助于版本向下兼容,特别是在产品级研发中。
import com.sfq.action.SignInfo;
import com.sfq.action.SignInfoFactory;
public class Client {
public static void main(String[] args) {
//初始化对象池
for (int i = 0; i < 4; i++) {
String subject = "科目" + i;
//初始化
for (int j = 0; j < 30; j++) {
String key = subject + "考试地点" + j;
SignInfoFactory.getSignInfo(key);
}
}
SignInfo signInfo = SignInfoFactory.getSignInfo("科目1考试地点1");
}
}
结果
......
科目3考试地点28-----建立对象,放到池中
科目3考试地点29-----建立对象,放到池中
科目1考试地点1-----直接从池中取得
(2)线程安全问题
只要使用Java开发都会遇到线程安全问题,享元模式有太大的几率发生线程不安全。假设,我们这里只使用考试科目作为对象池中对象的唯一标识,则对象池最多存在4个对象。那么,此时我们启动N多个线程来模拟,就会出现LiSi的编号,ZhangSan的考试地址的情况。
public class SignInfoFactory {
//池容器
private static HashMap<String,SignInfo> pool = new HashMap<String,SignInfo>();
//从池中获得对象
public static SignInfo getSignInfo(String key){
//设置返回对象
SignInfo result = null;
//池中没有该对象,则建立,并放入池中
if(!pool.containsKey(key)){
result = new SignInfo();
pool.put(key, result);
}else{
result = pool.get(key);
}
return result;
}
}
public class MultiThread extends Thread {
private SignInfo signInfo;
public MultiThread(SignInfo _signInfo){
this.signInfo = _signInfo;
}
public void run(){
if(!signInfo.getId().equals(signInfo.getLocation())){
System.out.println("编号:"+signInfo.getId());
System.out.println("考试地址:"+signInfo.getLocation());
System.out.println("线程不安全了!");
}
}
}
public class Client {
public static void main(String[] args) {
//在对象池中初始化4个对象
SignInfoFactory.getSignInfo("科目1");
SignInfoFactory.getSignInfo("科目2");
SignInfoFactory.getSignInfo("科目3");
SignInfoFactory.getSignInfo("科目4");
//取得对象
SignInfo signInfo = SignInfoFactory.getSignInfo("科目2");
while(true){
signInfo.setId("ZhangSan");
signInfo.setLocation("ZhangSan");
(new MultiThread(signInfo)).start();
signInfo.setId("LiSi");
signInfo.setLocation("LiSi");
(new MultiThread(signInfo)).start();
}
}
}
我们在使用享元模式时,对象池中的享元对象尽量多,多到足够满足业务为止。
(3)性能平衡
尽量使用Java基本类型作为外部状态。如果不考虑系统的修改风险,完全可以重新建立一个类作为外部状态,因为这才完全符合面向对象编程的理念。
public class ExtrinsicState {
//考试科目
private String subject;
//考试地点
private String location;
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
@Override
public boolean equals(Object obj){
if(obj instanceof ExtrinsicState){
ExtrinsicState state = (ExtrinsicState)obj;
return state.getLocation().equals(location) && state.getSubject().equals(subject);
}
return false;
}
@Override
public int hashCode(){
return subject.hashCode() + location.hashCode();
}
}
注意:一定要覆写equals和hashCode方法,否则它作为HashMap中的key值是根本没有意义的,只有hashCode值相等,并且equals返回结果为true,两个对象才相等,也只有在这种情况下才有可能从对象池中查找获得对象。
public class SignInfoFactory {
//池容器
private static HashMap<ExtrinsicState,SignInfo> pool = new HashMap <ExtrinsicState,SignInfo>();
//从池中获得对象
public static SignInfo getSignInfo(ExtrinsicState key){
//设置返回对象
SignInfo result = null;
//池中没有该对象,则建立,并放入池中
if(!pool.containsKey(key)){
result = new SignInfo();
pool.put(key, result);
}else{
result = pool.get(key);
}
return result;
}
}
public class Client {
public static void main(String[] args) {
//初始化对象池
ExtrinsicState state1 = new ExtrinsicState();
state1.setSubject("科目1");
state1.setLocation("上海");
SignInfoFactory.getSignInfo(state1);
ExtrinsicState state2 = new ExtrinsicState();
state2.setSubject("科目1");
state2.setLocation("上海");
//计算执行100万次需要的时间
long currentTime = System.currentTimeMillis();
for(int i=0;i<1000000;i++){
SignInfoFactory.getSignInfo(state2);
}
long tailTime = System.currentTimeMillis();
System.out.println("执行时间:"+(tailTime - currentTime) + " ms");
}
}
执行时间:172 ms
public class Client {
public static void main(String[] args) {
String key1 = "科目1上海";
String key2 = "科目1上海";
//初始化对象池
SignInfoFactory.getSignInfo(key1);
//计算执行10万次需要的时间
long currentTime = System.currentTimeMillis();
for(int i=0;i<10000000;i++){
SignInfoFactory.getSignInfo(key2);
}
long tailTime = System.currentTimeMillis();
System.out.println("执行时间:"+(tailTime - currentTime) + " ms");
}
}
执行时间:78 ms
外部状态最好以Java的基本类型作为标志,如String、int等,可以大幅地提升效率。