java map 线程安全_理解和讨论HashMap的线程安全

引发的问题,探讨在某些特定的多线程环境下是不是能直接使用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(当然这个可能性比较低....),那么以前写的旧代码就有可能出问题了。。。。

说到这里,希望各位高手、有经验的热心朋友可以指点一下,或一起探讨一下这个问题。。。谢谢

75a3bf8974c9bd5a56863a745113d139.gif

考虑了一下,多线程环境下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的操作。只要类似这种的处理方式,都应该可以使用。

这只是个人观点,请大家多提点意见、补充或纠正:-)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值