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是加锁了,但锁住的是本线程的任务对象,和另一个线程没有关系啊!!!(如果锁对了,其实这个方法也是可行的)
还有改了之后,目前可能出现空指针异常。