zoukankan      html  css  js  c++  java
  • Hashtable多线程遍历问题

    If a thread-safe implementation is not needed, it is recommended to use HashMap in place of code Hashtable. If a thread-safe highly-concurrent implementation is desired, then it is recommended to use ConcurrentHashMap in place of code Hashtable.

    如上一段摘自Hashtable注释。虽然Hashtable已经不被推荐使用了,但某种情况下还是会被使用。我们知道Hashtable与HashMap一个很大的区别就是是否线程安全,Hashtable相对于HashMap来说是线程安全的。但Hashtable在使用过程中,真的是线程安全么?

    最近在处理Wlan Framework中的一段逻辑,该部分逻辑使用了Hashtable存储设备列表。该设备列表在自己的工作线程中分别有添加、删除操作,并通过binder提供了查询操作。查询操作需要遍历设备列表,由于是通过binder跨进程调用的,因此获取列表的线程与添加、删除操作的线程并不是同一个线程,从而遇到了ConcurrentModificationException。Hashtable虽说是线程安全的,但是它仅仅是在添加、删除等操作时是线程安全的,如果遍历操作处理不好,同样会抛出异常。

    出问题的遍历方式如下

    1.  
      Iterator it;
    2.  
      it = mDeviceMap.keySet().iterator();
    3.  
      while(it.hasNext()) {
    4.  
      String key = (String)it.next();
    5.  
      ......
    6.  
      }
    7.  
       

    查看Hashtable源码,keySet返回的是Collections.SynchronizedSet对象。创建该对象时新建了一个KeySet对象,该KeySet为Hashtable的非静态内部类。此外还传入了Hashtable.this赋值给了SynchronizedSet的mutex,作为同步对象。

    1.  
      public Set<K> keySet() {
    2.  
      if (keySet == null)
    3.  
      keySet = Collections.synchronizedSet(new KeySet(), this);
    4.  
      return keySet;
    5.  
      }
    6.  
       

    如下为Collections.SynchronizedSet的实现,鉴于篇幅原因省略了部分方法及实现内容。

    1.  
      static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    2.  
      final Collection<E> c; // Backing Collection
    3.  
      final Object mutex; // Object on which to synchronize
    4.  
      SynchronizedCollection(Collection<E> c, Object mutex) {
    5.  
      this.c = Objects.requireNonNull(c);
    6.  
      this.mutex = Objects.requireNonNull(mutex);
    7.  
      }
    8.  
      public Object[] toArray() {
    9.  
      synchronized (mutex) {return c.toArray();}
    10.  
      }
    11.  
      public <T> T[] toArray(T[] a) {
    12.  
      synchronized (mutex) {return c.toArray(a);}
    13.  
      }
    14.  
       
    15.  
      public Iterator<E> iterator() {
    16.  
      return c.iterator(); // Must be manually synched by user!
    17.  
      }
    18.  
      }
    19.  
       

    如上mutex即为Hashtable的实例,与Hashtable中的add、remove等操方法用的是同一把锁。此外,通过注释可知,使用iterator遍历时,必须要自己进行同步操作。

    Hashtable遍历的方法

    Hashtable遍历的方法虽然有很多,但均是大同小异,这里主要介绍两种方案。
    第一种方案,通过Hashtable的源码可知,其put、remove等方法的同步是直接作用在方法上的,等价于使用Hashtable实例作为同步锁,因此如下遍历方式是线程安全的。

    1.  
      synchronized(mDeviceMap) {
    2.  
      Iterator it;
    3.  
      it = mDeviceMap.keySet().iterator();
    4.  
      while(it.hasNext()) {
    5.  
      String key = (String)it.next();
    6.  
      ......
    7.  
      }
    8.  
      }
    9.  
       

    由于使用迭代器遍历抛出异常的根本原因是expectedModCount != modCount,因此第二种方案便是不使用迭代器,而是重新创建一个数组,数组内容即是Hashtable中values保存的实例。这样的好处是无需自己再做同步,代码和逻辑看起来简洁,当然也会带来占用额外空间以及效率方面的代价。

    1.  
      int size = mDeviceMap.size();
    2.  
      Device[] devices = mDeviceMap.values().toArray(new Device[size]);
    3.  
      for (Device device: devices) {
    4.  
      Log.d(TAG, "name = " + device.mName);
    5.  
      ......
    6.  
      }
    7.  
       
    两种toArray转换的区别

    上面第二种遍历方式,在monkey测试的时候居然还是抛出了异常,只不过这次是Device变量空指针异常。看到这个异常的时候一脸的懵逼。Hashtable的put方法在最开始的时候明明对value判空了,key和value都不允许为空,那这个转换来的value数组为什么会有空的成员?

    虽然这个问题使用ConcurrentHashMap就可以避免,但总是要弄个明白心里才会踏实。那就一点点分析源码吧。

    既然是报Device为空,那就说明转换来的Device数组中有空成员。先分析mDeviceMap.values(),该方法同上面分析的keySet方法,返回的是SynchronizedCollection实例,这个应该没问题,那就继续分析后面的toArray方法了。

    1.  
      public Object[] toArray() {
    2.  
      synchronized (mutex) {return c.toArray();}
    3.  
      }
    4.  
      public <T> T[] toArray(T[] a) {
    5.  
      synchronized (mutex) {return c.toArray(a);}
    6.  
      }
    7.  
       

    通过上面可以看出这里的mutex便是Hashtable实例,c便是创建的Hashtable内部类ValueCollection的实例。SynchronizedCollection支持两种toArray方法,且均进行了同步,也就是整个转换过程中都有做同步操作。到这有点更懵了,既然做了同步,为啥还会有value为空的问题,只能接着往下看。上面c.toArray(a)调用的是ValueCollection的方法,ValueCollection继承自AbstractCollection,那就转到AbstractCollection的toArray(T[] a)方法。

    1.  
      public <T> T[] toArray(T[] a) {
    2.  
      // Estimate size of array; be prepared to see more or fewer elements
    3.  
      int size = size();
    4.  
      //注意,这里对传入的数组length与size做了比较
    5.  
      T[] r = a.length >= size ? a :
    6.  
      (T[])java.lang.reflect.Array
    7.  
      .newInstance(a.getClass().getComponentType(), size);
    8.  
      Iterator<E> it = iterator();
    9.  
      for (int i = 0; i < r.length; i++) {
    10.  
      if (! it.hasNext()) { // fewer elements than expected
    11.  
      if (a == r) {
    12.  
      r[i] = null; // null-terminate
    13.  
      } else if (a.length < i) {
    14.  
      return Arrays.copyOf(r, i);
    15.  
      } else {
    16.  
      System.arraycopy(r, 0, a, 0, i);
    17.  
      if (a.length > i) {
    18.  
      a[i] = null;
    19.  
      }
    20.  
      }
    21.  
      return a;
    22.  
      }
    23.  
      r[i] = (T)it.next();
    24.  
      }
    25.  
      // more elements than expected
    26.  
      return it.hasNext() ? finishToArray(r, it) : r;
    27.  
      }
    28.  
       

    注意到最终返回的是数组r,且在for循环中,确实有对r中内容赋值为null的情况,问题应该就出在这里了。如果我们调用toArray(T[] a)时,提供的数组a长度比实际长度大,多出的部分就会被null填充;如果数组a的长度比实际长度小,则会新建一个数组,并一一填充。

    那么最开始的空指针是怎么出现的呢?

    1.  
      int size = mDeviceMap.size();
    2.  
      Device[] devices = mDeviceMap.values().toArray(new Device[size]);
    3.  
       

    上面两条语句,虽然各自都进行了同步,但是这两条语句整体并未进行同步,当获取size之后,其他线程此时刚好调用了remove操作,进而导致在调用toArray的时候,实际size比我们提供的数组a的长度要小,从而导致返回的数组多出部分会被null填充。

    1.  
      public Object[] toArray() {
    2.  
      // Estimate size of array; be prepared to see more or fewer elements
    3.  
      Object[] r = new Object[size()];
    4.  
      Iterator<E> it = iterator();
    5.  
      for (int i = 0; i < r.length; i++) {
    6.  
      if (! it.hasNext()) // fewer elements than expected
    7.  
      return Arrays.copyOf(r, i);
    8.  
      r[i] = it.next();
    9.  
      }
    10.  
      return it.hasNext() ? finishToArray(r, it) : r;
    11.  
      }
    12.  
       

    再来看不带参数的toArray方法。该方法比较简单,直接根据实际的size创建数组,并进行填充。由于该方法调用时进行了同步,因此整个转换过程都是同步的,从而直接使用toArray()转换是线程安全的。

    总结

      1. Hashtable已经不推荐使用了,如果无需考虑线程安全,直接使用Hashmap;需要考虑线程安全时,使用ConcurrentHashMap。
      2. Hashtable遍历时,还是需要注意线程安全问题。
      3. SynchronizedCollection的两种toArray方法是不同的,如无特殊要求,建议使用无参的方法。
      4. 遇到问题要多看源码实现。
  • 相关阅读:
    第九章:Elasticsearch集群优化及相关节点配置说明
    Linux115条常用命令
    第一章:引擎介绍
    第七章:JAVA引用ESWCF及部分方法说明
    第八章:SCRT搭建ES搜索引擎步骤
    第三章:ES分词简单说明
    第二章:ES索引说明
    疑难杂症:Java中Scanner连续获取int和String型发生错误.
    java实现中缀表达式转后缀表达式
    编程题:输出字符集合的所有排列组合。
  • 原文地址:https://www.cnblogs.com/zqq-blog/p/10771978.html
Copyright © 2011-2022 走看看