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. 遇到问题要多看源码实现。

Hashtable多线程遍历问题的更多相关文章

  1. 一点一点看JDK源码(五)java.util.ArrayList 后篇之Spliterator多线程遍历

    一点一点看JDK源码(五)java.util.ArrayList 后篇之Spliterator多线程遍历 liuyuhang原创,未经允许禁止转载 本文举例使用的是JDK8的API 目录:一点一点看J ...

  2. JAVA的Hashtable在遍历时的迭代器线程问题

    这篇博客主要讲什么 Hashtable及其内部类的部分源码分析 Hashtable在遍历时的java.util.ConcurrentModificationException异常的来由和解决 单机在内 ...

  3. Java多线程遍历文件夹,广度遍历加多线程加深度遍历结合

    复习IO操作,突然想写一个小工具,统计一下电脑里面的Java代码量还有注释率,最开始随手写了一个递归算法,遍历文件夹,比较简单,而且代码层次清晰,相对易于理解,代码如下:(完整代码贴在最后面,前面是功 ...

  4. 关于HashTable的遍历方法解析

    要遍历一个Hashtable,api中提供了如下几个方法可供我们遍历: keys() - returns an Enumeration of the keys of this Hashtable ke ...

  5. Hashtable 数据遍历的几种方式

    Hashtable 在集合中称为键值对,它的每一个元素的类型是 DictionaryEntry,由于Hashtable对象的键和值都是Object类型,决定了它可以放任何类型的数据, 下面我就把Has ...

  6. C#中hashtable如何嵌套遍历

    嵌套hashtable的遍历取值怎么做 hastable中嵌套了hashtable,想用递归的方式把所有hashtable中的key和value取出来 foreach (DictionaryEntry ...

  7. .Net 中HashTable,HashMap 和 Dictionary<key,value> 和List<T>和DataTable的比较

    参考资料 http://www.cnblogs.com/MichaelYin/archive/2011/02/14/1954724.html http://zhidao.baidu.com/link? ...

  8. HashMap HashTable HashSet

    原文转载自 http://blog.csdn.net/wl_ldy/article/details/5941770 HashMap是新框架中用来代替HashTable的类 也就是说建议使用HashMa ...

  9. HashMap与HashTable的区别、HashMap与HashSet的关系

    http://blog.csdn.net/wl_ldy/article/details/5941770 HashTable的应用非常广泛,HashMap是新框架中用来代替HashTable的类,也就是 ...

  10. Java集合(8):Hashtable

    一.Hashtable介绍 和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射,它在很大程度上和HashMap的实现差不多. Hashtable ...

随机推荐

  1. Pytest 固件

    一.固件使用背景 在执行测试用例时,我们常常需要在测试用例执行的前后去完成一些额外的操作.例如针对于 Web 测试,在用例执行前需要打开浏览器,完成用户登录等一系列前置操作:在用例执行完成后,要清除浏 ...

  2. WPF 实现雪花效果

    雪花控件类: class MM:Control { DispatcherTimer dispatcherTimer = new DispatcherTimer(); public MM() { dis ...

  3. 「SOL」序列计数sequence (模拟赛)

    看了题解过后觉得好像有点熟悉--但是总之是想不起来怎么做的了,自己的做法也和题解不一样. 是一道很好的题啦,把两种做法都写一下作个总结. 题面 给定一个长度为 \(n\) (\(n\le 10^5\) ...

  4. SQL Server创建dblink跨库查询

    dblink是跨库查询的主要手段,在Oracle创建DbLink中已经演示了Oracle中如何创建及使用DbLink,这篇博客看看SQL Server中如何使用. 一.通过图形化界面直接创建 选择当前 ...

  5. 路飞项目 day03 前端配置、后台主页、项目依赖问题

    一.路飞项目前端配置 1.先删除一些不要的 ​ 删除多余的组件,只要app和首页组件 ​ 然后改一下组件的内部代码 -App.vue中 ______________ <template> ...

  6. Hyperf微服务

    https://hyperf.wiki 状态码含义 https://segmentfault.com/a/1190000002523655

  7. 如何用python脚本采集某网图片

    一.前言: 今天学了两个工具urlopen  和etree,这两个小工具至关重要.urllib.request模块提供了最基本的构造HTTP请求的方法,利用它可以模拟浏览器的一个请求发起过程,同时它还 ...

  8. make vscode portable together with its extensions

    0. the goal make vscode poratable together with its extensions, so that the offline pc could make us ...

  9. windows2012安装.net4.7.2

    第一步,下载.net4.7.2安装包 离线包:https://download.visualstudio.microsoft.com/download/pr/1f5af042-d0e4-4002-9c ...

  10. js——带暂停、启动功能的定时

    简单的封装,将 interval 二次封装,对外提供暂停.启动功能. 不足之处:interval定时间隔是固定的,在调用异步函数的时候,可能会出现bug.例如:在调用ajax异步请求过程中,发送a.b ...