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

这篇博客主要讲什么

  • Hashtable及其内部类的部分源码分析
  • Hashtable在遍历时的java.util.ConcurrentModificationException异常的来由和解决
  • 单机在内存中缓存数据并定期清除过期缓存的简单实现

事情的起因

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

 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()时

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

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

SynchronizedSet类的部分源码如下:

public <T> T[] toArray(T[] a) {
    synchronized(mutex) {return c.toArray(a);}
}
public Iterator<E> iterator() {
    return c.iterator(); // Must be manually synched by user!
}
public boolean add(E e) {
    synchronized(mutex) {return c.add(e);}
}
public boolean remove(Object o) {
    synchronized(mutex) {return c.remove(o);}
}

代码中变量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调用删除对象

55         cacheMap.remove(key);

改为在迭代器中删除对象

55         i.remove();

即可。

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

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

时间: 2025-01-01 11:02:25

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

java 集合遍历时删除元素

本文探讨集合在遍历时删除其中元素的一些注意事项,代码如下 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 import java.util.ArrayList; import java.util.Iterator; import java

Java集合遍历时删除

public static void main(String[] args){ List<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(3); list.add(4); list.add(5); Iterator<Integer> interator = list.iterator(); while(interator.hasNext()){ Integer i

Java 集合Hashtable源码深入解析

概要 前面,我们已经系统的对List进行了学习.接下来,我们先学习Map,然后再学习Set:因为Set的实现类都是基于Map来实现的(如,HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的). 首先,我们看看Map架构.如上图:(01) Map 是映射接口,Map中存储的内容是键值对(key-value).(02) AbstractMap 是继承于Map的抽象类,它实现了Map中的大部分API.其它Map的实现类可以通过继承AbstractMap来减少重复编码.(

Java集合02----LinkedList的遍历方式及应用

                                          Java集合02----LinkedList的遍历方式及应用 前面已经学习了ArrayList的源码,为了学以致用,故列举一些ArrayList的遍历方式及应用.JDK源码学习系列05----LinkedList 1.LinkedList的遍历方式 a.一般的for循环(随机访问) int size = list.size(); for (int i=0; i<size; i++) { list.get(i);

java 深入HashTable

在java中与有两个类都提供了一个多种用途的hashTable机制,他们都可以将可以key和value结合起来构成键值对通过put(key,value)方法保存起来,然后通过get(key)方法获取相对应的value值.一个是前面提到的HashMap,还有一个就是马上要讲解的HashTable.对于HashTable而言,它在很大程度上和HashMap的实现差不多,如果我们对HashMap比较了解的话,对HashTable的认知会提高很大的帮助.他们两者之间只存在几点的不同,这个后面会阐述. 一

java.util.HashMap和java.util.HashTable (JDK1.8)【转】

一.java.util.HashMap 1.1 java.util.HashMap 综述 java.util.HashMap继承结构如下图 HashMap是非线程安全的,key和value都支持null HashMap的节点是链表,节点的equals比较的是节点的key和value内容是否相等. 1 static class Node<K,V> implements Map.Entry<K,V> { 2 final int hash; 3 final K key; 4 V valu

Java 多维数组遍历详解

数组是Java中的一种容器对象,它拥有多个单一类型的值.当数组被创建的时候数组长度就已经确定了.在创建之后,其长度是固定的.下面是一个长度为10的数组: public class ArrayDemo { private int arraySize=10; public int[] arrayOfIntegers = new int[arraySize]; } 上面的代码是一维数组的例子.换句话说,数组长度只能在一个方向上增长.很多时候我们需要数组在多个维度上增长.这种数组我们称之为多维数组.为简

STL容器遍历时删除元素

STL容器遍历时在循环体内删除元素最容易出错了,根本原因都是因为迭代器有效性问题,在此记下通用删除方法,该方法适用于所有容器: 1 std::vector<int> myvec; 2 3 std::vector<int>::iterator it = myvec.begin(); 4 while( it != myvec.end()) 5 { 6 it = myvec.erase(it); 7 } 容器list有个比较另类的删除方法,如下代码所示: std::list<int

Java集合01----ArrayList的遍历方式及应用

                                             Java集合01----ArrayList的遍历方式及应用 1.ArrayList的遍历方式 a.一般for循环(随机访问) Integer value = null; int size = list.size(); for (int i=0; i<size; i++) { value = (Integer)list.get(i); } b.增强型for循环(for-each) Integer value