ConcurrentHashMap被认为是支持高并发、高吞吐量的线程安全一个HashMap实现,因此多线程开发中经常使用到,但是最近在开发中却遇到了数据不一致问题,给自己埋了个大坑,下面描述下问题:
首先是工作场景描述:有一个订单列表,每个订单又包含多种类型的任务,每个线程一次只能处理一种类型的任务(取所有订单的该类型的任务,进行批量处理,任务没有先后关系),某订单处理完毕后,修改订单状态。
代码如下:
public class TaskRunner implements Runnable{ //订单id列表 private final List<String> orderList; //订单与任务类型列表对应关系,key是订单id,value是任务列表 private final ConcurrentHashMap<String, List<String>> contentMap; //所有的任务类型 private final ConcurrentLinkedQueue<String> typeQueue; public TaskRunner(ConcurrentHashMap<String, List<String>> contentMap, ConcurrentLinkedQueue<String> type, List<String> orderList) { this.contentMap = contentMap; this.typeQueue = type; this.orderList = orderList; } @Override public void run() { while(true){ String type = typeQueue.poll(); if(type != null){ try { //do something to finish the task... Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } //任务处理完毕后,修改对应的订单列表 for(String order : orderList){ try{ if(contentMap.get(order) != null){ contentMap.get(order).remove(type); if(contentMap.get(order)!= null && contentMap.get(order).size() == 0){ //订单order处理完毕 contentMap.remove(order); } } }catch(Exception e){ e.printStackTrace(); } } } if(contentMap.size() > 0){ System.out.println(contentMap.size() +", queue size:"+typeQueue.size()); }else{ System.out.println("empty, queue size:"+typeQueue.size()); } } } }
代码逻辑很简单,就是当任务处理完毕后,从订单列表中将任务移除,最终期望的结果应该是:任务类型队列typeQueue为空,所有的订单与任务映射contentMap为空。
contentMap初始化:任务列表为一个ArrayList
ConcurrentHashMap<String, List<String>> contentMap = new ConcurrentHashMap<String, List<String>>(); List<String> types = new ArrayList<String>(); type.add("b01"); type.add("b02"); type.add("b03"); for(String zqgs : orderList){ contentMap.put(zqgs, types); }
启动2个线程跑TaskRunner
int tn = 2; ExecutorService service = Executors.newFixedThreadPool(tn); for(int i = 0; i < tn; i++){ service.execute(new TaskRunner(contentMap, typeQueue, orderList)); }
运行3-5次就会出现,任务类型队列typeQueue为空,但订单与任务映射contentMap提示还有若干订单没有完成,这是说不通的(推测是不同步造成的),于是乎对for循环做了个修改,如下(加了synchronized关键字)。
for(String order : orderList){ synchronized(this){ try{ if(contentMap.get(order) != null){ contentMap.get(order).remove(type); if(contentMap.get(order)!= null && contentMap.get(order).size() == 0){ contentMap.remove(order); } } }catch(Exception e){ e.printStackTrace(); } } }
再次执行,还是遇到了数据不一致。
于是乎上网查了下ConcurrentHashMap的一些实现原理,它利用了分段锁来提高并发性能,它支持完全并发的读以及一定程度并发的写,即写是分段锁,读操作不是加锁的。
ConcurrentHashMap包含若干段(Segment),每个段又有多个HashEntry,HashEntry存放我们put进去的数据,结构如下:
static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; }
啥?value是volatile的,在并发写的情况下就会出现不一致的情况啊,如果是synchronized的就没问题了。
可参考下面的连接,就能比较好的理解这两个关键字了:
volatile 与 synchronized 区别 和 volatile与synchronized关键字
于是乎修改对contentMap的初始化方法:
ConcurrentHashMap<String, List<String>> contentMap = new ConcurrentHashMap<String, List<String>>(); List<String> types = Collections.synchronizedList(new ArrayList<String>()); type.add("b01"); type.add("b02"); type.add("b03"); for(String zqgs : orderList){ contentMap.put(zqgs, types); }
这样,不管运行多少次,当任务类型队列typeQueue为空,订单与任务映射contentMap也变为空了。
注:至于为什么,for循环我加了synchronized关键字,依然不能保证数据的一致性,为啥呢?原因有点逗比,synchronized是加锁了,但锁住的是本线程的任务对象,和另一个线程没有关系啊!!!(如果锁对了,其实这个方法也是可行的)
还有改了之后,目前可能出现空指针异常。
相关推荐
java源码剖析-ConcurrentHashMap
ConcurrentHashMap源码剖析
ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的...
Java利用ConcurrentHashMap实现本地缓存demo; 基本功能有缓存有效期、缓存最大数、缓存存入记录、清理线程、过期算法删除缓存、LRU算法删除、获取缓存值等功能。 复制到本地项目的时候,记得改包路径哦~
源码分析见我博文:http://blog.csdn.net/wabiaozia/article/details/50684556
ConcurrentHashMap具体是怎么实现线程安全的呢,肯定不可能是每个方法加synchronized,那样就变成了HashTable。
解析concurrenthashmap的源码,学习多线程的思想
主要介绍了Java中遍历ConcurrentHashMap的四种方式详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
本文将结合Java内存模型,分析JDK源代码,探索ConcurrentHashMap高并发的具体实现机制,包括其在JDK中的定义和结构、并发存取、重哈希和跨段操作,并着重剖析了ConcurrentHashMap读操作不需要加锁和分段锁机制的内在...
这时候ConcurrentHashMap达到容量扩容而忽略了ReservationNode情况,调用put的时候在synchronized(f)没有对ReservationNode处理,所以会出现死循环。 在jdk1.8和1.9中对比 http://gee.cs.oswego.edu/cgi- bin/...
需要注意的是,在使用ConcurrentHashMap时,并发更新和迭代操作可能会导致一些问题,因为在操作过程中其他线程可能会修改数据。因此,如果需要保证精确的操作顺序或避免并发更新带来的问题,可以考虑使用更高级的...
ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。ConcurrentHashMap为了提高本身的并发能力,在内部采用了...
java本地缓存ConcurrentHashMap
程序员面试加薪必备_ConcurrentHashMap底层原理与源码分析深入详解
ConcurrentHashMap之实现细节
concurrenthashmap1.7的源码分析
ConcurrentHashMap的实现原理(纯干货)
Java——并发容器之ConcurrentHashMap;Java——并发容器之ConcurrentHashMap;Java——并发容器之ConcurrentHashMap;Java——并发容器之ConcurrentHashMap;Java——并发容器之ConcurrentHashMap;Java——并发...
java7-8中的 HashMap和ConcurrentHashMap全解析
Java并发编程之ConcurrentHashMap Java并发编程之ConcurrentHashMap.pdf