🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
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所示。 ![](https://box.kancloud.cn/2016-08-14_57b0036c3c7aa.jpg) 图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等,可以大幅地提升效率。