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是HashMap的一个线程安全版本,设计目标是在高并发场景下提供高效的数据访问。...它在保证数据一致性的同时,避免了全表锁定,从而提升了并发性能,是Java并发编程中不可或缺的工具。
同时,对Node加锁保证了在并发环境下的数据一致性。这种精细的并发控制策略使得JDK1.8的ConcurrentHashMap成为并发编程中高效且可靠的工具。 总结来说,JDK1.8的ConcurrentHashMap在设计上兼顾了线程安全和性能,...
- **不可变性**:在`ConcurrentHashMap`中,散列表的大部分元素(键、散列值和下一个元素的引用)都被设计为不可变的,这意味着一旦创建就无法改变。这种设计可以保证即使在没有锁的情况下读取散列表也不会出现问题...
`HashMap`是非线程安全的,意味着在多线程环境下,多个线程同时操作`HashMap`可能会导致数据不一致或者死循环。因此,如果需要在并发环境中使用,必须使用同步机制,如`synchronized`关键字或`Collections....
同时,内部的`HashEntry`链表结构以及`CAS`操作确保了在链表遍历和插入过程中的数据一致性。这种设计使得`ConcurrentHashMap`在高并发场景下性能优越,是Java并发编程中不可或缺的数据结构之一。
与传统的 HashMap 不同,ConcurrentHashMap 使用了分段锁(Segment)机制,允许在多个线程同时进行读写操作时,实现高并发性,而不会导致数据的不一致。 2. **分段锁设计**: ConcurrentHashMap 将整个散列表分成...
### HashMap和ConcurrentHashMap...综上所述,面试时通常会针对HashMap和ConcurrentHashMap的实现原理、它们的差异、以及在并发环境下如何保证数据的一致性和线程安全进行提问。了解这些知识点有助于在面试中脱颖而出。
为了在多线程环境中确保数据的一致性和完整性,Java提供了多种线程安全的解决方案。 标题中的"如何保证集合是线程安全的"这个问题,可以从两个主要角度回答。首先,Java提供了一种简单粗暴的方法,即使用...
1. 多线程环境下并发修改:在多线程环境下,如果多个线程同时对HashMap进行读写操作,可能会导致数据的不一致性和数据丢失。例如,当一个线程正在执行put操作时,另一个线程可能同时在进行get或remove操作,这可能...
然后,文章深入探讨了Java多线程编程中数据安全的问题,包括线程对象中的数据安全、线程同步机制对数据安全的影响、数据一致性问题等。 在Java多线程编程中,数据安全问题是一个非常重要的问题。由于多个线程并发...
6. **原子性操作**:`ConcurrentHashMap`提供了诸如`putIfAbsent()`这样的原子操作,确保在插入元素时,如果键不存在,可以安全地插入,而不会因并发问题导致数据冲突。 在决定何时使用`ConcurrentHashMap`时,应...
【并发控制】`ConcurrentHashMap`利用了`volatile`关键字确保多线程环境下数据的一致性,并使用CAS(Compare and Swap)操作进行无锁更新,减少了锁的使用,提升了并发性能。 【源码分析】深入理解`...
ConcurrentHashMap 底层实现机制分析 在本文中,我们将深入探索 ConcurrentHashMap 的高并发实现机制,并分析其在 Java 内存模型基础上的实现原理。了解 ConcurrentHashMap 的实现机制有助于我们更好地理解 Java ...
数据字典在软件开发中扮演着关键角色,它有助于提高代码的可读性、可维护性和数据一致性。以下是一些与Java中数据字典相关的知识点: 1. **类和对象**:Java是面向对象的语言,数据字典的概念可以映射到类的设计上...
在上面的代码示例中,尽管两个线程同时修改 ConcurrentHashMap,但由于其内在的线程安全性,我们不会观察到类似 HashMap 中的数据一致性问题。 此外,ConcurrentHashMap 还提供了其他高级特性,如支持原子操作的 ...
2. 集群部署,数据可能存在不一致:需要应用服务保证数据一致性。 3. 应用重启,缓存数据丢失。 本地缓存应用场景 1. 数据量不大。 2. 修改频率低、甚至是静态的数据。 3. 查询 QPS 高:通过纯内存操作,避免网络...
8. **哈希表的并发控制**:哈希表在并发环境下可能遇到哈希碰撞和数据一致性问题。使用如 ConcurrentHashMap(Java)之类的并发哈希表可以保证在多线程环境下的正确性。 9. **事务与分布式事务**:在大型分布式售票...
综上所述,`ConcurrentHashMap`的读操作之所以不需要加锁,是因为它利用了`volatile`关键字保证了数据的可见性,结合无锁的CAS操作确保了数据的一致性,以及细粒度的锁策略降低了锁的竞争,从而实现了高效的并发读取...
线程安全是指在多线程环境中,一个类或方法可以被多个线程同时访问而不会导致数据不一致或者意外的结果。`HashMap`在设计时并未考虑线程安全,因此在并发场景下,如果不采取适当的同步措施,可能会遇到诸如数据丢失...