引发的问题,探讨在某些特定的多线程环境下是不是能直接使用HashMap?在哪些特定的并发环境下能否正常使用呢?
众所周知HashMap不是线程安全的,但到底HashMap在什么情况才不是线程安全的? 查看HashMap的源码,内部有一个modCount变量,在put、remove、等等进行结构性修改时改变这个值。在Hash Iterator中记录expectedModCount变量,在遍历或者删除时比较modCount与expectedModCount的值是否相等,不相等就抛出ConcurrentModificationException,类似一种乐观所的机制,代码如下:final Entry nextEntry() {
[color=red]if (modCount != expectedModCount)
throw new ConcurrentModificationException();[/color]
Entry e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
public void remove() {
if (current == null)
throw new IllegalStateException();
[color=red]if (modCount != expectedModCount)
throw new ConcurrentModificationException();[/color]
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
那就是说,在多线程环境下是操作HashMap的内部类迭代器的时(Iterator的remove与遍历),就有可能出现并发的问题。那假设我现在有一个HashMap,保证不对HashMap的迭代器操作,在多线程环境下单纯地调用HashMap的get、remove、put、containsKey等方法,是不是就不会出现并发的问题呢?
static Random random = new Random();
static HashMap map = new HashMap();
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException ex) {
System.err.println("t1 catch the InterruptedException...");
}
int key = random.nextInt(1000);
map.put(key, String.valueOf(key));
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException ex) {
System.err.println("t2 catch the InterruptedException...");
}
int key = random.nextInt(1000);
map.get(key);
}
}
};
t2.start();
Thread t3 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException ex) {
System.err.println("t3 catch the InterruptedException...");
}
Integer key = random.nextInt(1000);
map.containsKey(key);
}
}
};
t3.start();
Thread t4 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException ex) {
System.err.println("t4 catch the InterruptedException...");
}
int key = random.nextInt(1000);
map.remove(key);
}
}
};
t4.start();
}
经过测试之后,代码能正确运行,暂且不考虑数据的是否100%正确,最少没有抛出ConcurrentModificationException。原因是,上面代码中的4种操作都没有直接操作到HashMap内部的迭代器。
当然如果加上操作迭代器的代码,异常马上就出来了
Thread t5 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException ex) {
System.err.println("t5 catch the InterruptedException...");
}
for (Integer key : map.keySet()) {
}
}
}
};
t5.start();
但在多线程环境下,HashMap的get、put、remove、containsKey这样的操作是否就正确并且保证一定没有问题!?(甚至保证对同一个key的get、put、remove、containsKey的操作是单线程的前提下,多线程是对于不同key)(否则就算多线程对相同的key操作最多也是会出现数据的不一致性,如对相同的key进行多次的put操作,后一个覆盖了前一个....)
带着查看一下JavaDoc关于HashMap同步的描述:
注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:
JavaDoc的说法也是有点模糊,如果对HashMap多线程访问,从结构上修改了该映射就必须保持同步?但测试代码中的put、remove操作不也是结构性修改的动作吗?到了这里问题变得有点难以解释和理清思路了。。。
跳出以上描述重新考虑这个问题,其中一种想法是:上面是建立在关心HashMap内部实现的基础上去讨论和测试的,但API文档已经说明HashMap并非线程安全的,不要在多线程环境下对HashMap进行结构性的动作,去冒将来不确定的时间发生任意不确定行为的风险。比如在某个版本会为了某种原因而去修改HashMap的内部实现,单纯调用HashMap的put、remove操作也会抛出ConcurrentModificationException(当然这个可能性比较低....),那么以前写的旧代码就有可能出问题了。。。。
说到这里,希望各位高手、有经验的热心朋友可以指点一下,或一起探讨一下这个问题。。。谢谢
考虑了一下,多线程环境下put还是可能会有问题
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry e = table[bucketIndex];
table[bucketIndex] = new Entry(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
发生在index相同的情况下,大家拿到的链头可能不是最新的,后一个会直接覆盖了前一个
而且当多条线程检测到容量超过负载因子时,会能发生多次resize。
结构性修改总是采用持有,替换的方式去操作,因此操作内部数组与单向链表的时候不会抛出NullPointerException
//======================add at 5.1=================================
小结一下:remove与put都是一样的,由于大家拿到的不是最新链头,只要大家在Entry数组的index相同时(经过hash后的index),就有可能出现后一个覆盖前一个的操作,即前一个的操作无效。
可能产生的现象会是:
1)put进行的data有可能丢失了
2)一些通过remove(Object key)删除掉的元素(返回删除成功)又出来了。
3)多线程检测到HashMap容量超过负载因子时会进行多次的resize,由于要rehash,所以消耗的性能也是巨大的。
但如果put、与remove,是在同一条线程中处理,而get发生在多线程环境,是不会出现上述3种情况或抛出ConcurrentModificationException的。唯一可能出现的是返回不是最新的数据(可能拿到刚好被remove的,拿不到刚好put进去的),但这种情况即使ConcurrentHashMap也会出现。但ConcurrentHashMap的get方法多了一步处理,看起来好像更精确.
V get(Object key, int hash) {
if (count != 0) { // read-volatile
HashEntry e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
V readValueUnderLock(HashEntry e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
当调用ConcurrentHashMap的get方法时,找到key所对应的Entry而getValue为null时会调用readValueUnderLock(e)方法上分离锁取一次。这具体会发生什么情况下会发生呢且readValueUnderLock(e)能生效呢?网上有两种比较常见的说法:
1)另外一条线程在修改节点
2)HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序,或许源出自http://www.iteye.com/topic/344876
查看ConcurrentHashMap源码能看到put方法其实是创建一个新的节点并以之前的第一个节点为这个节点的next节点(即这个新创建的节点为单向链表的第一个节点),然后
tab[index] = new HashEntry(key, hash, first, value);执行赋值
remove方法则是重新把整条单向链表(除被删除Entry)全部拷贝一次执行赋值for (HashEntry p = first; p != e; p = p.next)
newFirst = new HashEntry(p.key, p.hash,
newFirst, p.value);
tab[index] = newFirst;
所有,既不是另外一条线程正在修改某节点,也不是重新排序。最后追踪到源码的注释:
引用
This is possible only if a
compiler happens to reorder a HashEntry initialization with
its table assignment, which is legal under memory model
but is not known to ever occur.
原来还是Java内存模型的问题(Jmm),执行tab[index] = newFirst或tab[index] = new HashEntry(key, hash, first, value);时,Entry已经被认为初始化ok并返回给table(在堆中分派一块空间),这是正好有其他线程调用到get方法,获取到这个刚“实例化好”的单向链表,但由于JMM初始化的无序性,当你get这个链表Entry中的value 时,他有可能还没完全初始化好!或按注释所说不知道发生过这样的初始化但这是合法的。。。。这和为什么Java的单例模式不能使用双重检查加锁的原因类似,都是由于Java内存模型(JMM)的无序性。这里有部分介绍http://rjx2008.iteye.com/admin/blogs/342474
数据一致性的问题扯远了,回到上述:但如果put、与remove,是在同一条线程中处理,而get发生在多线程环境,是不会出现上述3种情况或抛出ConcurrentModificationException的。
最少有两种的适用场合
1)类似委托的机制
2)类似命令模式的执行方式
这两种处理get,put时方式是很接近的,就是多线程接收到要删除、添加数据的指令
但最终会由一条线程去过滤这些指令,判断、分析、处理,最后单线程地对HashMap执行put、remove的操作。只要类似这种的处理方式,都应该可以使用。
这只是个人观点,请大家多提点意见、补充或纠正:-)