【问题标题】:JAVA - Concurrency and locksJAVA - 并发和锁
【发布时间】:2016-12-23 10:30:35
【问题描述】:

我需要像这样创建一个哈希映射:

Map<String, List<Integer>>

我正在使用 2 个线程。

这里是输出地图的示例:

---------------------
| xxx | a  b  c  d  |
| yyy | a' b' c' d' |
| zzz | A  B  C  D  |
---------------------

两个线程都参与了条目的创建;因此,例如,线程 1 有以下数据集要存储:

-----------------------------------
| 1 |  (xxx,a)  (yyy,a')  (zzz,A) |
| 2 |  (xxx,b)  (yyy,b')  (zzz,B) |
-----------------------------------

线程 2 有:


| 1 |  (xxx,c)  (yyy,c')  (zzz,C) |
| 2 |  (xxx,d)  (yyy,d')  (zzz,D) |
-----------------------------------

因此,在同一时刻,两个线程可能会计算相同的密钥:线程 1 (xxx,a) 和线程 2 (xxx,c)。

我可以使用的最佳锁定/解锁机制是什么?

我尝试了以下一个

ReadWriteLock lock = new ReentrantReadWriteLock();

executor.submit(() -> {
   lock.writeLock().lock();
   try {
      sleep(1);
      map.put("foo", "bar");
  } finally {
     lock.writeLock().unlock();
  }

});

但我不想锁定整个哈希映射。我想创建一个锁,如果并且仅当处理期间的两个线程都应该在同一个键上工作。

谢谢

【问题讨论】:

    标签: java multithreading locking


    【解决方案1】:

    首先,您应该使用并发 Map:

    Map<String, List<Integer>> map = new ConcurrentHashMap<>();
    

    其次,您应该使用多个线程访问 之前 的同步列表填充它,如下所示:

    for (String key : keys) {
        map.put(key, Collections.synchronizedList(new ArrayList<>()));
    }
    

    或者你及时这样做:

    List<Integer> list = map.get(key);
    if (list == null) {
        map.putIfAbsent(key, Collections.synchronizedList(new ArrayList<>()));
        list = map.get(key);
    }
    

    就是这样。现在一切都应该是线程安全的。

    注意:在从地图中获取列表之前不要使用列表,这一点很重要。由于并发性,多个线程可能会尝试将缺失列表添加到地图中,但只有一个线程会成功。因此,在尝试添加缺失列表后,请始终从地图中获取它。

    【讨论】:

    • 如果您事先不知道键是什么会怎样?
    • 您也可以使用map.putIfAbsent(),跳过初始化阶段。但是您必须每次在访问该值之前都这样做。但是您可以通过尝试获取进行优化,并且仅当获取失败时,putIfAbsent()
    • 我为您提到的替代方案添加了示例代码,但事先不知道密钥。
    • @Harmlezz 正如您在上一个答案中所写,list.add(value) 应该在 if 语句之外完成,不是吗?
    • @Fab 我不明白你的意思。在我发布所有代码之后,list.add() 可以安全地完成。您可以在获得存储在地图中的列表之后添加一些东西,而不是之前。列表的双重获取很重要,以防其他线程之前添加了丢失的列表。
    【解决方案2】:

    没有必要重新发明轮子,java.util.concurrent 包已经包含了Map 接口的线程安全实现 - ConcurrentHashMap

    但是,您还必须同步您的列表(请参阅下面 BretC 的评论)。所以可能最方便的解决方案是使用Guava synchronized multimap。像这样:

    public static void main(String[] args) {
    
        Multimap <String, Integer> map = Multimaps.synchronizedMultimap(HashMultimap.<String, Integer> create());
    
        // In Thread 1
        map.put("foo", 1);
    
        //In Thread 2
        map.put("foo", 2);
    
        System.out.println(map); // prints {foo=[1, 2]}
    }
    

    【讨论】:

    • 如果线程 1 读取映射以查看密钥是否存在(但它不存在)然后线程 2 读取映射以查看是否存在相同的密钥(但仍然不存在)会发生什么...不会一个线程覆盖另一个线程吗?
    【解决方案3】:

    不确定其他答案是否会满足您的要求,因为您需要从地图中读取(以查看密钥是否已经存在),然后在原子操作中进行更新。

    不确定这是否是最优雅的解决方案,但您可以使用类似的方法...

    导入 java.util.HashSet; 导入 java.util.Map; 导入 java.util.Set; 导入 java.util.concurrent.ConcurrentHashMap;

    public class KeyLockMap<K, V> {
    
        private Map<K, V> map;
    
        private Set<K> lockKeys;
    
        public KeyLockMap() {
            map = new ConcurrentHashMap<K, V>();
            lockKeys = new HashSet<K>();
        }
    
        public void lock(K key) throws InterruptedException {
            synchronized(lockKeys) {
                // Keep waiting while the lockKeys set contains the key we want to update
                while(lockKeys.contains(key)) {
                    lockKeys.wait();
                }
            }
        }
    
        public void unlock(K key) {
            synchronized(lockKeys) {
                lockKeys.remove(key);
                // Notify any threads that may be waiting for this key to be removed
                lockKeys.notifyAll();
            }
        }
    
        public Map<K, V> getMap() {
            return map;
        }
    }
    

    现在在你的线程中,你可以做...

    keyLockMap.lock(KEY);
    Map<String, List<Integer>> map = keyLockMap.getMap();
    ...do updates...
    keyLockMap.unlock(KEY);
    

    希望这会有所帮助。


    另一种可行的方法是在 ConcurrentHashMap 上使用“putIfAbsent”,例如...

    List<Integer> newValue = new CopyOnWriteArrayList<>();
    List<Integer> value = concurrentMap.putIfAbsent(KEY, newValue);
    
    if(value == null) {
        value = newValue;
    }
    
    value.add(DATA);
    

    如果另一个线程首先执行putIfAbsent,您将获得由该线程创建的CopyOnWriteArrayList,并且您将能够向其中添加数据,因为CopyOnWriteArrayList 是线程安全的。

    (已编辑以说明第一次放置时返回 null...)

    【讨论】:

    • 如果使用 java 8,您可以只使用 ConcurrentHashMapputIfAbsent,然后使用 CopyOnWrite 列表作为值...
    • 拜托,能否更好地解释一下 CopyOnWrite 步骤?
    • Java 7 中引入的 ConcurrentHashMap
    • @Panner 实际上在 Java 5 中 :)
    • @BretC value = concurrentMap.putIfAbsent(KEY, value); 在创建新密钥后返回 null。所以下一个value.add(DATA) 可能会失败。
    【解决方案4】:

    您应该使用 Hashtable 而不是 Hashmap。 Hashtable 是线程安全的(已经同步),不需要再实现锁/解锁机制。

    【讨论】:

    • 那么,我可以将相同的(共享的)数据结构传递给两个线程,以便每个线程都可以存储自己的数据部分吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-08
    • 2016-12-29
    • 1970-01-01
    • 2011-02-04
    • 2011-06-11
    • 2018-10-11
    相关资源
    最近更新 更多