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

查看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. }

如下为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. public Iterator<E> iterator() {
  15. return c.iterator(); // Must be manually synched by user!
  16. }
  17. }

如上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. }

由于使用迭代器遍历抛出异常的根本原因是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. }
两种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. }

通过上面可以看出这里的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. }

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

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

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

上面两条语句,虽然各自都进行了同步,但是这两条语句整体并未进行同步,当获取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. }

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

总结

  1. Hashtable已经不推荐使用了,如果无需考虑线程安全,直接使用Hashmap;需要考虑线程安全时,使用ConcurrentHashMap。
  2. Hashtable遍历时,还是需要注意线程安全问题。
  3. SynchronizedCollection的两种toArray方法是不同的,如无特殊要求,建议使用无参的方法。
  4. 遇到问题要多看源码实现。

转载于:https://www.cnblogs.com/zqq-blog/p/10771978.html

Hashtable多线程遍历问题相关推荐

  1. java多线程 文件夹_Java多线程遍历文件夹,广度遍历加多线程加深度遍历结合

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

  2. HashTable详解、源码、扩容、深入理解HashTable、HashTable多线程并发问题

    Hashtable 简介 和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射. Hashtable 继承于Dictionary,实现了Map.Cl ...

  3. Python多线程遍历爬取FTP文件(附可实现源码)

    目录 应用目标 思路分析 1.扫描网段 2.远程建立FTP连接 3.遍历读取写入文件 完整源码(可运行) 写在最后 应用目标 1.扫描网段,获取其中所有的开放FTP服务的机器的IP地址 2.依次遍历获 ...

  4. C# hashTable的遍历【2种方法】与排序【3种方法】

    private void Form1_Load(object sender, EventArgs e) { Hashtable ht = new Hashtable(); ht.Add("j ...

  5. Hashtable的遍历

    foreach( DictionaryEntry de in hashTable) {  console.WriteLine("Key -- {0}; Value --{1}.", ...

  6. java Hashtable的遍历方法

    今天遇到用hashtable取值时发现取得的值是排过序的,而我并希望它排序输出,因为它是倒序输出,查一下原因是因为我用下面的第一种方法取值的,将hashtable的值排序输出了,这个真是我疏忽的问题, ...

  7. java 多线程遍历list_如何线程安全地遍历List:Vector、CopyOnWriteArrayList

    遍历List的多种方式 在讲如何线程安全地遍历List之前,先看看通常我们遍历一个List会采用哪些方式. 方式一: for(int i = 0; i < list.size(); i++) { ...

  8. Hashtable的遍历(DictionaryEntry)

    定义可设置或检索的字典键/值对. 命名空间:System.Collections 程序集:mscorlib(在 mscorlib.dll 中) foreach 语句是对枚举数的包装,它只允许从集合中读 ...

  9. .Net 中HashTable,HashMap 和 Dictionarykey,value 和ListT和DataTable的比较

    转载自http://www.cnblogs.com/jilodream/p/4219840.html (一)HashTable    和Dic    数据结构 Hashtable和Dictionary ...

最新文章

  1. kafka的安装与启动运行
  2. Python 技术篇-用paramiko库实现winodws本地文件上传至linux服务器实例演示
  3. 滴滴算法大赛算法解决过程 - 拟合算法
  4. 【产品】阿里产品经理内训:能力模型解读
  5. Python基础入门:正则re.sub使用自定义替换方法
  6. LVM---基本创建和使用
  7. 不要自称是程序员,我十多年的 IT 职场总结
  8. Python初学者的自我修养,找到自己的方向
  9. 柴犬为什么总是被卡住狗头?
  10. 简单实现ToolStripMenuItem(菜单栏)的单选效果
  11. 前端es6文档大全,你想要的这都有
  12. 报表生成器FastReport .Net基本信息介绍
  13. 网站优质内容细则及示例说明
  14. 新联盟呼吁结束种族主义人工智能研究,声称将面Kong与犯罪行为相匹配
  15. 烤仔TVのCCW丨存储押金代付
  16. html5青蛙过河,[推荐]===PS4上的本地多人游戏推荐心得===家庭聚会,欢乐时光 (持续更新)...
  17. python操作excel编号自增加1
  18. 优秀笔记软件盘点—好看、强大的可视化笔记软件、知识图谱工具
  19. 冯·诺依曼结构与哈佛结构浅析
  20. 安全性设计之-ip白名单设计

热门文章

  1. 概述nodejs核心机制
  2. 命令passwd报错因inode节点处理记录
  3. C#中数据类型的安全转换(is,as)
  4. 获得本页面URL地址
  5. 汉字内码UNICODE转换表
  6. css布局,让侧边栏高度撑满,并且不会随着屏幕内容滚动
  7. 阿里巴巴为什么要禁止使用存储过程?
  8. Cookie或将被替换!Chrome工程师提议新型HTTP状态管理协议
  9. UNIX:缓冲区和重定向
  10. python里的os模块_python中os模块再回顾