我有一个方法可以从队列中提供地图,只有在地图大小不超过某个数字时才会这样做。这引发了并发问题,因为我从每个线程获得的大小都是非连贯的全局。我通过这段代码复制了这个问题
import java.sql.Timestamp;
import java.util.Date;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrenthashMapTest {
private ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<Integer, Integer>();
private ThreadUx[] tArray = new ThreadUx[999];
public void parallelMapFilling() {
for ( int i = 0; i < 999; i++ ) {
tArray[i] = new ThreadUx( i );
}
for ( int i = 0; i < 999; i++ ) {
tArray[i].start();
}
}
public class ThreadUx extends Thread {
private int seq = 0;
public ThreadUx( int i ) {
seq = i;
}
@Override
public void run() {
while ( map.size() < 2 ) {
map.put( seq, seq );
System.out.println( Thread.currentThread().getName() + " || The size is: " + map.size() + " || " + new Timestamp( new Date().getTime() ) );
}
}
}
public static void main( String[] args ) {
new ConcurrenthashMapTest().parallelMapFilling();
}
}
通常我应该只有一行输出且大小不超过1,但我确实有这样的东西
Thread-1 || The size is: 2 || 2016-06-07 18:32:55.157
Thread-0 || The size is: 2 || 2016-06-07 18:32:55.157
我尝试将整个run方法标记为已同步,但只有在我执行此操作时才会起作用
@Override
public void run() {
synchronized ( map ) {
if ( map.size() < 1 ) {
map.put( seq, seq );
System.out.println( Thread.currentThread().getName() + " || The size is: " + map.size() + " || " + new Timestamp( new Date().getTime() ) );
}
}
}
它有效,为什么只有同步块工作和同步方法?此外,我不想使用与同步块一样旧的东西,因为我正在使用Java EE应用程序,是否有Spring或Java EE任务执行器或注释可以提供帮助?
答案 0 :(得分:3)
您正在使用site:elastic.co/blog/
,并根据API文档:
请记住聚合状态方法的结果包括 size ,isEmpty和containsValue通常仅在地图时才有用 没有在其他线程中进行并发更新。否则 这些方法的结果反映了可能足够的瞬态 用于监控或估算目的,但不用于程序控制。
这意味着除非您明确同步ConcurrentHashMap
的访问权限,否则无法获得准确的结果。
将size()
添加到synchronized
方法不起作用,因为线程没有在同一个锁对象上进行同步 - 每个锁都会自动锁定。
在地图上进行同步本身确实有效,但恕我直言,这不是一个好选择,因为这会失去run
可以提供的性能优势。
总之,您需要重新考虑设计。
答案 1 :(得分:3)
来自Java Concurrency in Practice:
在整个地图上运行的
ConcurrentHashMap
方法的语义,例如size
和isEmpty
,已经略微削弱,以反映集合的并发性质。由于大小的结果在计算时可能已经过时,因此它实际上只是一个估计值,因此允许大小返回近似值而不是精确计数。虽然起初这可能看起来很令人不安,但实际上size
和isEmpty
等方法在并发环境中的用处要小得多,因为这些数量是移动目标。因此,这些操作的要求被削弱,以便为最重要的操作(主要是get
,put
,containsKey
和remove
启用性能优化。
synchronized
Map
实现提供但不是ConcurrentHashMap
提供的一项功能是锁定地图以进行独占访问。使用Hashtable
和synchronizedMap
,获取Map锁可防止任何其他线程访问它。在异常情况下这可能是必要的,例如以原子方式添加多个映射,或者多次迭代Map并且需要以相同的顺序查看相同的元素。但总的来说,这是一个合理的权衡:应该预期并发收集会不断改变其内容。
解决方案:
重构设计,不要将size
方法用于并发访问。
要将方法用作size
和isEmpty
,您可以使用同步收集Collections.synchronizedMap
。同步集合通过序列化对集合状态的所有访问来实现其线程安全性。这种方法的成本是并发性差;当多个线程争用集合范围的锁时,吞吐量会受到影响。此外,您还需要将其检查和放置的块与map实例同步,因为它是复合操作。
第三。使用第三方实现或编写自己的实现。
public class BoundConcurrentHashMap <K,V> {
private final Map<K, V> m;
private final Semaphore semaphore;
public BoundConcurrentHashMap(int size) {
m = new ConcurrentHashMap<K, V>();
semaphore = new Semaphore(size);
}
public V get(V key) {
return m.get(key);
}
public boolean put(K key, V value) {
boolean hasSpace = semaphore.tryAcquire();
if(hasSpace) {
m.put(key, value);
}
return hasSpace;
}
public void remove(Object key) {
m.remove(key);
semaphore.release();
}
// approximation, do not trust this method
public int size(){
return m.size();
}
}
类BoundConcurrentHashMap
与ConcurrentHashMap
一样有效且几乎是线程安全的。因为删除元素并在remove
方法中释放信号量不是应该同时进行的。但在这种情况下,它是可以容忍的。 size
方法仍会返回近似值,但put
方法不允许超出地图大小。