这篇博客主要讲什么

事情的起因

工作中需要在某个业务类中设置一个将一些对象缓存在内存中的一个缓存机制(单机)。于是有了以下类似结构的实现:

  1. package org.cnblog.test;
  2.  
  3. import java.util.Hashtable;
  4. import java.util.Iterator;
  5.  
  6. /**
  7. * JAVA的Hashtable在遍历时的迭代器线程问题
  8. * @author HY
  9. */
  10. public class HashtableIteratorTest {
  11.  
  12. //初始化缓存,并启动刷新缓存的事件。
  13. static {
  14. Cache.cacheMap = new Hashtable<String, Long>();
  15. new Cache().start();
  16. }
  17.  
  18. /**
  19. * 执行Main方法
  20. * @param args
  21. */
  22. public static void main(String[] args) {
  23.  
  24. Thread t = new Thread(new Runnable() {
  25. public void run() {
  26. while (true) {
  27. long time = System.currentTimeMillis();
  28. Cache.cacheMap.put(time + "", time);
  29. System.out.println("[" + Thread.currentThread().getName() + "]Cache中新增缓存>>" + time);
  30. try {
  31. // 每秒钟增加一个缓存实例。
  32. Thread.sleep(1*1000);
  33. } catch (InterruptedException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. }
  38. });
  39. t.start();
  40. }
  41.  
  42. private static class Cache extends Thread {
  43. private static Hashtable<String, Long> cacheMap;
  44.  
  45. /**
  46. * 刷新缓存的方法,清除时间超过10秒的缓存。
  47. */
  48. private void refresh() {
  49. synchronized (cacheMap) {
  50. String key;
  51. Iterator<String> i = cacheMap.keySet().iterator();
  52. while (i.hasNext()) {
  53. key = i.next();
  54. if (cacheMap.get(key) != null && System.currentTimeMillis() - cacheMap.get(key) > 10*1000) {
  55. cacheMap.remove(key);
  56. System.out.println("[" + Thread.currentThread().getName() + "]删除的Key值<<" + key);
  57. }
  58. }
  59. }
  60. }
  61.  
  62. public void run() {
  63. while (true) {
  64. refresh();
  65. try {
  66. // 每过10秒钟作一次缓存刷新
  67. Thread.sleep(10*1000);
  68. } catch (InterruptedException e) {
  69. e.printStackTrace();
  70. }
  71. }
  72. }
  73. }
  74. }

业务类HashtableIteratorTest中,使用静态内部类Cache来存储缓存,缓存的直接载体为内部类中的静态成员cacheMap。

内部类Cache为线程类,线程的执行内容为每10秒钟进行一次缓存刷新。(刷新结果是清除掉缓存时间超过10秒的内容)

业务类HashtableIteratorTest在初始化时,启动内部类的线程,并实现一些存入缓存和读取缓存的方法。

代码中的main方法模拟每秒钟增加一个缓存。

于是,代码遇到了以下问题:

[Thread-1]Cache中新增缓存>>1418207644572
[Thread-1]Cache中新增缓存>>1418207645586
[Thread-1]Cache中新增缓存>>1418207646601
[Thread-1]Cache中新增缓存>>1418207647616
[Thread-1]Cache中新增缓存>>1418207648631
[Thread-1]Cache中新增缓存>>1418207649646
[Thread-1]Cache中新增缓存>>1418207650661
[Thread-1]Cache中新增缓存>>1418207651676
[Thread-1]Cache中新增缓存>>1418207652690
[Thread-1]Cache中新增缓存>>1418207653705
[Thread-0]删除的Key值<<1418207644572
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.Hashtable$Enumerator.next(Unknown Source)
at org.cnblog.test.HashtableIteratorTest$Cache.refresh(HashtableIteratorTest.java:53)
at org.cnblog.test.HashtableIteratorTest$Cache.run(HashtableIteratorTest.java:64)

上述代码第53行,迭代缓存Map的时候抛出了java.util.ConcurrentModificationException异常。

解决过程

首先,ConcurrentModificationException在JDK中的描述为:

当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常。

很奇怪,我明明在refresh()中对cacheMap遍历时,已经对cacheMap对象加锁,可是在next的时候仍然抛出了这个异常。

于是查看JDK源码,发现:

在cacheMap.keySet()时

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

KeySet是Set接口的一个子类,是Hashtable的内部类。返回的是将KeySet经过加锁后的包装类SynchronizedSet的对象。

SynchronizedSet类的部分源码如下:

  1. public <T> T[] toArray(T[] a) {
  2. synchronized(mutex) {return c.toArray(a);}
  3. }
  4. public Iterator<E> iterator() {
  5. return c.iterator(); // Must be manually synched by user!
  6. }
  7. public boolean add(E e) {
  8. synchronized(mutex) {return c.add(e);}
  9. }
  10. public boolean remove(Object o) {
  11. synchronized(mutex) {return c.remove(o);}
  12. }

代码中变量c为KeySet对象,mutex为调用keySet()方法的对象,即加锁的对象为cacheMap。(Collections同步Set的原理

注意代码中iterator()方法中的注释:用户必须手动同步!

于是笔者仿佛找到了一些头绪。

在获取迭代器时,cacheMap.keySet().iterator():

KeySet的iterator()方法最终返回的是Enumerator的对象,Enumerator是Hashtable的内部类。以下截取重要代码:

  1. public T next() {
  2. if (modCount != expectedModCount)
  3. throw new ConcurrentModificationException();
  4. return nextElement();
  5. }
  6.  
  7. public void remove() {
  8. if (!iterator)
  9. throw new UnsupportedOperationException();
  10. if (lastReturned == null)
  11. throw new IllegalStateException("Hashtable Enumerator");
  12. if (modCount != expectedModCount)
  13. throw new ConcurrentModificationException();
  14.  
  15. synchronized(Hashtable.this) {
  16. Entry[] tab = Hashtable.this.table;
  17. int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;
  18.  
  19. for (Entry<K,V> e = tab[index], prev = null; e != null;
  20. prev = e, e = e.next) {
  21. if (e == lastReturned) {
  22. modCount++;
  23. expectedModCount++;
  24. if (prev == null)
  25. tab[index] = e.next;
  26. else
  27. prev.next = e.next;
  28. count--;
  29. lastReturned = null;
  30. return;
  31. }
  32. }
  33. throw new ConcurrentModificationException();
  34. }
  35. }

可以看到,问题的发生源头找到了,当modCount != expectedModCount时,就会抛出异常。

那么,modCount和expectedModCount是做什么的?

modCount和expectedModCount是int型

modCount字段在其外部类Hashtable中,注释的大概意思是:这个数字记录了,对hashtable内部结构产生变化的操作次数。如rehash()、put(K key, V value)中,都会有modCount++。

expectedModCount字段在Enumerator类中,并在Enumerator(迭代器)初始化时,赋予modCount的值。其注释的主要内容为:用于检测并发修改。

其值在迭代器的remove()方法中,与modCount一同自增(见上述代码中remove()方法中第22、23行)。

于是真相浮于水面:在获得迭代器时,expectedModCount与modCount值相等,但迭代的同时,第55行的cacheMap.remove(key)使modCount值自增1,导致modCount != expectedModCount,于是抛出ConcurrentModificationException异常。

结果

由上面的结论得出:

在Hashtable迭代的过程中,除迭代器中的操作外,凡对该map对象有产生结构变化的操作时,属于并发修改。迭代器将不能正常工作。

这就是此类Hashtable在遍历时,抛出ConcurrentModificationException异常的来由,用加锁同步两个操作不是问题所在。

本文问题解决方法很简单:将55行的使用map调用删除对象

  1. cacheMap.remove(key);

改为在迭代器中删除对象

  1. i.remove();

即可。

也以此推断出此类异常的解决方式:

要么不要在迭代的时候进行rehash()、put(K key, V value)、remove(Object key)等会对map结构产生变化的操作;要么就在迭代器中做可能的操作。

JAVA的Hashtable在遍历时的迭代器线程问题的更多相关文章

  1. 【Java】List遍历时删除元素的正确方式

    当要删除ArrayList里面的某个元素,一不注意就容易出bug.今天就给大家说一下在ArrayList循环遍历并删除元素的问题.首先请看下面的例子: import java.util.ArrayLi ...

  2. java list集合遍历时删除元素

    转: java list集合遍历时删除元素 大家可能都遇到过,在vector或arraylist的迭代遍历过程中同时进行修改,会抛出异常java.util.ConcurrentModification ...

  3. java 集合list遍历时删除元素

    本文探讨集合在遍历时删除其中元素的一些注意事项,代码如下 import java.util.ArrayList; import java.util.Iterator; import java.util ...

  4. Java遍历时删除List、Set、Map中的元素(源码分析)

    在对List.Set.Map执行遍历删除或添加等改变集合个数的操作时,不能使用普通的while.for循环或增强for.会抛出ConcurrentModificationException异常或者没有 ...

  5. 分析轮子(八)- List.java 各种遍历方式及遍历时移除元素的方法

    注:玩的是JDK1.7版本 1:先尝栗子,再分析,代码简单,注释清晰,可自玩一下 /** * @description:测试集合遍历和移除元素的方式 * @author:godtrue * @crea ...

  6. Java:Hashtable

    概要 前一章,我们学习了HashMap.这一章,我们对Hashtable进行学习.我们先对Hashtable有个整体认识,然后再学习它的源码,最后再通过实例来学会使用Hashtable.第1部分 Ha ...

  7. 【原理探究】女朋友问我ArrayList遍历时删除元素的正确姿势是什么?

    简介 我们在项目开发过程中,经常会有需求需要删除ArrayList中的某个元素,而使用不正确的删除方式,就有可能抛出异常.或者在面试中,会遇到面试官询问遍历时如何正常删除元素.所以在本篇文章中,我们会 ...

  8. java.util.HashMap和java.util.HashTable (JDK1.8)

    一.java.util.HashMap 1.1 java.util.HashMap 综述 java.util.HashMap继承结构如下图 HashMap是非线程安全的,key和value都支持nul ...

  9. Java中ArrayList循环遍历并删除元素的陷阱

    ava中的ArrayList循环遍历并且删除元素时经常不小心掉坑里,昨天又碰到了,感觉有必要单独写篇文章记一下. 先写个测试代码: import java.util.ArrayList; public ...

随机推荐

  1. 使用Google-Authenticator加强serverSSH登录

    对于须要特殊加密的人群,我这里给出对应的方法来进行谷歌式加密. 过程例如以下: 准备: 首先在你的手机上准备好client(自己百度下载) 接下来依照命令做: date 查看系统时间       da ...

  2. Unity 配置静态excel 工作流程

    TP:FCEE652B cause how improvement cause 在游戏开发的过程中,很多时候需要策划填的一些静态数据表(比如英雄表,技能表等等),而策划一般都习惯使用excel. ex ...

  3. Unity多媒体展示项目经验分享-ImageEffect+动态绑定

    Unity多媒体展示项目经验分享-ImageEffect+动态绑定+网络通信 <ignore_js_op> “海尔科技展墙”是去年年初我们为上海家电博览会制作的一个多媒体展项,有限的工期以 ...

  4. 【Python3 爬虫】16_抓取腾讯视频评论内容

    上一节我们已经知道如何使用Fiddler进行抓包分析,那么接下来我们开始完成一个简单的小例子 抓取腾讯视频的评论内容 首先我们打开腾讯视频的官网https://v.qq.com/ 我们打开[电视剧]这 ...

  5. 【DB2】国标行业分类存储,通过SQL查询出层级关系

    新建表 DROP TABLE Industry; CREATE TABLE Industry( IndustryCode VARCHAR(40),IndustryName VARCHAR(100),P ...

  6. Android Framework 分析---2消息机制Native层

    在Android的消息机制中.不仅提供了供Application 开发使用的java的消息循环.事实上java的机制终于还是靠native来实现的.在native不仅提供一套消息传递和处理的机制,还提 ...

  7. SGDMA

    Scatter-gather DMA 使用一个链表描述物理上不连续的存储空间,然后把链表首地址告诉DMA master.DMA master在传输完一块物理连续的数据后,不用发起中断,而是根据链表来传 ...

  8. JS DOM -- 关于回车键盘事件执行事件

    一.需求制作一个模拟对话框, 二. 1.需要发送后,输入框清空 2.按enter键可发送 三.代码部分 <!DOCTYPE HTML> <html> <head> ...

  9. FAT32和NTFS最大支持的单个文件大小分别是多大?

    以前,RAID控制器和文件系统都有2TB的限制.控制器上的限制,主要是指每个LUN不能大于2TB,而磁盘组一般则没有这个限制:文件系统的限制主要是指每个分区(partition)不能大于2TB,不过3 ...

  10. ASP.NET MVC 表单提交多层子级实体集合数据到控制器中

    于遇到了项目中实体类嵌套多层子级实体集合,并且子级实体集合的数据需要提交保存到数据库中的问题.针对此情况需要进行一些特殊的处理才可以将整个 实体类及子级实体集合数据提交表单到控制器中,解决的方法是根据 ...