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. 链接mysql数据库报错:2003-cant connect to Mysql server on ‘localhost’(10038)

    今天用navicat连mysql时候突然报错了 我百度了一下,知道了是mysql服务没开,但是我打开了服务,发现服务里面没有mysql,于是就去cmd以管理员身份打开命令提示符,切换到安装mysql的 ...

  2. url 编码解码

    from urllib import parse#url编码url = 'http://www.baidu.com?query = python基础教程'url_str = parse.quote_p ...

  3. python脚本打包

    python脚本打包 Python写脚本很方便,可以直接在机器上运行,但有时候脚本源码不方便透露或是其他机器不支持的原因,需要将其打包成可执行文件,需要用到 pyinstaller 首先下载pip s ...

  4. springcloud day01

    单体架构:业务所有功能都在一个项目中开发,打成一个包部署 优点是架构简单 部署成本低 缺点是 耦合度高 分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为一个独立的项目开发,也称为一个服务 优 ...

  5. 使用viper读取配置文件

    配置文件config.yml mysql: type: mysql dsn: "user:pass@tcp(localhost:30306)/db_name?charset=utf8& ...

  6. nanoPi R1 资料

    eflasher脱机烧写 在命令行终端中通过执行下列命令进行烧写: $ su root $ eflasher root 用户的密码是 fa.   串口登录 控制台波特率 115200

  7. 【git】git子模块操作-添加子模块与克隆子模块

    https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%AD%90%E6%A8%A1%E5%9D%97 git submodule upda ...

  8. CSS入门学习笔记

    CSS入门学习笔记一.CSS简介1.什么是CSS?2.为什么使用CSS?3.CSS的作用二.CSS语法1.CSS基础语法2.CSS注释语法3.CSS应用方法三.CSS选择器1.元素选择器2.类选择器3 ...

  9. vue页面点击按钮增加和删除标签

    1.在data()里定义一个空数组: conditions:[], 2.给数组一个容器,并且遍历数组 <div v-for="item in conditions">& ...

  10. 解析bean封装成BeanDefinition

    默认命名空间 1:parseDefaultElement 从代码中可以了解到默认的命名空间的一节节点主要是4种,import,alias,bean,beans private void parseDe ...