JAVA的Hashtable在遍历时的迭代器线程问题
这篇博客主要讲什么
- Hashtable及其内部类的部分源码分析
- Hashtable在遍历时的java.util.ConcurrentModificationException异常的来由和解决
- 单机在内存中缓存数据并定期清除过期缓存的简单实现
事情的起因
工作中需要在某个业务类中设置一个将一些对象缓存在内存中的一个缓存机制(单机)。于是有了以下类似结构的实现:
package org.cnblog.test; import java.util.Hashtable;
import java.util.Iterator; /**
* JAVA的Hashtable在遍历时的迭代器线程问题
* @author HY
*/
public class HashtableIteratorTest { //初始化缓存,并启动刷新缓存的事件。
static {
Cache.cacheMap = new Hashtable<String, Long>();
new Cache().start();
} /**
* 执行Main方法
* @param args
*/
public static void main(String[] args) { Thread t = new Thread(new Runnable() {
public void run() {
while (true) {
long time = System.currentTimeMillis();
Cache.cacheMap.put(time + "", time);
System.out.println("[" + Thread.currentThread().getName() + "]Cache中新增缓存>>" + time);
try {
// 每秒钟增加一个缓存实例。
Thread.sleep(1*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
} private static class Cache extends Thread {
private static Hashtable<String, Long> cacheMap; /**
* 刷新缓存的方法,清除时间超过10秒的缓存。
*/
private void refresh() {
synchronized (cacheMap) {
String key;
Iterator<String> i = cacheMap.keySet().iterator();
while (i.hasNext()) {
key = i.next();
if (cacheMap.get(key) != null && System.currentTimeMillis() - cacheMap.get(key) > 10*1000) {
cacheMap.remove(key);
System.out.println("[" + Thread.currentThread().getName() + "]删除的Key值<<" + key);
}
}
}
} public void run() {
while (true) {
refresh();
try {
// 每过10秒钟作一次缓存刷新
Thread.sleep(10*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
业务类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的内部类。以下截取重要代码:
public T next() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
return nextElement();
}
public void remove() {
if (!iterator)
throw new UnsupportedOperationException();
if (lastReturned == null)
throw new IllegalStateException("Hashtable Enumerator");
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
synchronized(Hashtable.this) {
Entry[] tab = Hashtable.this.table;
int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index], prev = null; e != null;
prev = e, e = e.next) {
if (e == lastReturned) {
modCount++;
expectedModCount++;
if (prev == null)
tab[index] = e.next;
else
prev.next = e.next;
count--;
lastReturned = null;
return;
}
}
throw new ConcurrentModificationException();
}
}
可以看到,问题的发生源头找到了,当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调用删除对象
cacheMap.remove(key);
改为在迭代器中删除对象
i.remove();
即可。
也以此推断出此类异常的解决方式:
要么不要在迭代的时候进行rehash()、put(K key, V value)、remove(Object key)等会对map结构产生变化的操作;要么就在迭代器中做可能的操作。
JAVA的Hashtable在遍历时的迭代器线程问题的更多相关文章
- 【Java】List遍历时删除元素的正确方式
当要删除ArrayList里面的某个元素,一不注意就容易出bug.今天就给大家说一下在ArrayList循环遍历并删除元素的问题.首先请看下面的例子: import java.util.ArrayLi ...
- java list集合遍历时删除元素
转: java list集合遍历时删除元素 大家可能都遇到过,在vector或arraylist的迭代遍历过程中同时进行修改,会抛出异常java.util.ConcurrentModification ...
- java 集合list遍历时删除元素
本文探讨集合在遍历时删除其中元素的一些注意事项,代码如下 import java.util.ArrayList; import java.util.Iterator; import java.util ...
- Java遍历时删除List、Set、Map中的元素(源码分析)
在对List.Set.Map执行遍历删除或添加等改变集合个数的操作时,不能使用普通的while.for循环或增强for.会抛出ConcurrentModificationException异常或者没有 ...
- 分析轮子(八)- List.java 各种遍历方式及遍历时移除元素的方法
注:玩的是JDK1.7版本 1:先尝栗子,再分析,代码简单,注释清晰,可自玩一下 /** * @description:测试集合遍历和移除元素的方式 * @author:godtrue * @crea ...
- Java:Hashtable
概要 前一章,我们学习了HashMap.这一章,我们对Hashtable进行学习.我们先对Hashtable有个整体认识,然后再学习它的源码,最后再通过实例来学会使用Hashtable.第1部分 Ha ...
- 【原理探究】女朋友问我ArrayList遍历时删除元素的正确姿势是什么?
简介 我们在项目开发过程中,经常会有需求需要删除ArrayList中的某个元素,而使用不正确的删除方式,就有可能抛出异常.或者在面试中,会遇到面试官询问遍历时如何正常删除元素.所以在本篇文章中,我们会 ...
- java.util.HashMap和java.util.HashTable (JDK1.8)
一.java.util.HashMap 1.1 java.util.HashMap 综述 java.util.HashMap继承结构如下图 HashMap是非线程安全的,key和value都支持nul ...
- Java中ArrayList循环遍历并删除元素的陷阱
ava中的ArrayList循环遍历并且删除元素时经常不小心掉坑里,昨天又碰到了,感觉有必要单独写篇文章记一下. 先写个测试代码: import java.util.ArrayList; public ...
随机推荐
- Python——@property属性描述符
@property 可以将python定义的函数“当做”属性访问,从而提供更加友好访问方式,但是有时候setter/getter也是需要的 假设定义了一个类Cls,该类必须继承自object类,有一私 ...
- 在LoadRunner脚本中实现随机ThinkTime
一般情况下,我们都是通过Run-Time Settings来设置Think Time(思考时间),可以设置回放脚本时忽略思考时间,或者是设置回放随机的一段思考时间. By default, when ...
- SpringBoot Tips
读取资源文件 @RestController @EnableAutoConfiguration public class ResourcesController { @Autowired privat ...
- 51单片机 | 1-Wire总线及应用实例
———————————————————————————————————————————— 1-Wire总线 - - - - - - - - - - - - - - - - - - - - - - - ...
- 每日一个机器学习算法——k近邻分类
K近邻很简单. 简而言之,对于未知类的样本,按照某种计算距离找出它在训练集中的k个最近邻,如果k个近邻中多数样本属于哪个类别,就将它判决为那一个类别. 由于采用k投票机制,所以能够减小噪声的影响. 由 ...
- Oracle 存储过程调用返回游标的另一个存储过程。
一个扩展存储过程调用另一个存储过程,示例: 被调用存储过程:最后会返回一个游标,游标返回一个值.调用这个存储过程的存储过程同样需要获取它. procedure SearchBill --根据到货单号查 ...
- IE 页面不正常显示 错误脚本不报错 脚本调试相关
在开发时,有时自己做的页面上的JS有错误,但是IE浏览器并不报错,这个时候有可能是因为脚本调试被禁止了. 在Internet选项的高级里面有 两个禁止脚本调试选项,把他们去掉就行.
- C语言-gdb调试工具详解
回车 重复上一次命令 产生可调试的可执行文件:gcc -g main.c -o main, 必须加上-g选线, 表示在可执行文件中加入源文件信息, 但并不是将源文件嵌入可执行文件, 所以在调试时必须保 ...
- 使用swap 清空vector
//最简单的使用swap,清除元素并回收内存 vector <int>().swap(vecInt); //清除容器并最小化它的容量, // vecInt.swap(vector<i ...
- vivado与modelsim的联合仿真(一)
vivado软件中也自带仿真工具,但用了几天之后感觉仿真速度有点慢,至少比modelsim慢挺多的.而modelsim是我比较熟悉的一款仿真软件,固然选它作为设计功能的验证.为了将vivado和mod ...