这篇博客主要讲什么
- 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的时候仍然抛出了这个异常。
在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结构产生变化的操作;要么就在迭代器中做可能的操作。