首页 » 设计模式之禅(第2版) » 设计模式之禅(第2版)全文在线阅读

《设计模式之禅(第2版)》28.4 享元模式的扩展

关灯直达底部

28.4.1 线程安全的问题

线程安全是一个老生常谈的话题,只要使用Java开发都会遇到这个问题,我们之所以要在今天的享元模式中提到该问题,是因为该模式有太大的几率发生线程不安全,为什么呢?

我们还以报考系统为例来说明这个问题。大家有没有想过,为什么要以考试科目+考试地点作为外部状态呢?为什么不能以考试科目或者考试地点作为外部状态呢?这样池中的对象会更少!可以!完全可以!我们把程序以考试科目为外部状态,把享元工厂稍作修改,如代码清单28-10所示。

代码清单28-10 报考信息工厂

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;     }}  

下面做很小的改动,只修改了黑色字体部分。为了展示多线程的情况,我们写一个多线程的类,如代码清单28-11所示。

代码清单28-11 多线程场景

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(/"线程不安全了!/");     }     }}  

在run方法中判断特殊值,检查是否是线程安全,我们来看看场景类,如代码清单28-12所示。

代码清单28-12 场景类

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;     }     }}  

模拟实际的多线程情况,在对象池中我们保留4个对象,然后启动N多个线程来模拟,我们马上就看到如下的提示:

编号:LiSi

考试地址:ZhangSan

线程不安全了!

看看,线程不安全了吧,这是正常的,设置的享元对象数量太少,导致每个线程都到对象池中获得对象,然后都去修改其属性,于是就出现一些不和谐数据。只要使用Java开发,线程问题是不可避免的,那我们怎么去避免这个问题呢?享元模式是让我们使用共享技术,而Java的多线程又有如此问题,该如何设计呢?没什么可以参考的标准,只有依靠经验,在需要的地方考虑一下线程安全,在大部分的场景下都不用考虑。我们在使用享元模式时,对象池中的享元对象尽量多,多到足够满足业务为止。

28.4.2 性能平衡

尽量使用Java基本类型作为外部状态。在报考系统中,我们不考虑系统的修改风险,完全可以重新建立一个类作为外部状态,因为这才完全符合面向对象编程的理念。好,我们实现处理,先看类图,如图28-4所示。

图28-4 类作为外部状态

我们首先来看ExtrinsicState外部状态类,如代码清单28-13所示。

代码清单28-13 外部状态类

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,两个对象才相等,也只有在这种情况下才有可能从对象池中查找获得对象。

注意 如果把一个对象作为Map类的键值,一定要确保重写了equals和hashCode方法,否则会出现通过键值搜索失败的情况,例如map.get(object)、map.contains(object)等会返回失败的结果。

SignInfo的修改较小,仅在SignInfo中引入该ExtrinsicState外部状态对象,在此不再赘述。我们再来看享元工厂,它是以ExtrinsicState类作为外部状态,如代码清单28-14所示。

代码清单28-14 享元工厂

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;     }}  

重点是看看我们的场景类,我们来测试一下性能差异,如代码清单28-15所示。

代码清单28-15 场景类

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

同样,我们看看以String类型作为外部状态的运行情况,如代码清单28-16所示。

代码清单28-16 场景类

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

看到没?一半的效率,这还是非常简单的享元对象,看看我们重写的equals方法和hashCode方法,这段代码是必须实现的,如果比较复杂,这个时间差异会更大。

各位,想想看,使用自己编写的类作为外部状态,必须覆写equals方法和hashCode方法,而且执行效率还比较低,这种吃力不讨好的事情最好别做,外部状态最好以Java的基本类型作为标志,如String、int等,可以大幅地提升效率。