【问题标题】:Calling get() and put() method of Hashmap correctly.正确调用 Hashmap 的 get() 和 put() 方法。
【发布时间】:2014-11-24 19:53:30
【问题描述】:

下面的代码:

public ReentrantReadWriteLock getLock(String tableName) {
    ReentrantReadWriteLock lock = locksMap.get(tableName);
    if (lock == null) {
        lock = new ReentrantReadWriteLock();
        locksMap.put(tableName, lock);
    }
}

//其中locksMap是一个HashMap,键为String(tableName),值为ReentrantReadWriteLock(Lock)。

我的问题是,如果线程同时访问这个方法,它们会得到具有相同“tableName”的不同Lock对象,因为Map的get和put方法是分开调用的。

任何带有解释的解决方案将不胜感激? 提前致谢。

【问题讨论】:

  • 您可能想在这里使用synchronized
  • ConcurrentHashMap 有一个 putIfAbsent() 原子方法。自 Java 8 以来甚至更好:一个原子的 computeIfAbsent() 方法。
  • @njzk2 谢谢,考虑使用同步块,但不确定使用 ConcurrentHashMap 还是同步块更有效?
  • 只是一般性评论,您最好使用一个包含锁和其他东西的“表”对象实例。然后你可以传递它,只需要 map.get() 它一次(而不是每个操作,包括表锁)。

标签: java multithreading concurrency hashmap


【解决方案1】:

使用ConcurrentMap 通常会比synchronized 块产生更好的性能。

Java 5-7:

ConcurrentMap<String, ReadWriteLock> lockMap = new ConcurrentHashMap<>();

ReadWriteLock getLock(String key) {
    ReadWriteLock lock = lockMap.get(key);
    if (lock == null) {
        lock = new ReentrantReadWriteLock();
        ReadWriteLock other = lockMap.putIfAbsent(key, lock);
        if (other != null) {
            // another thread's putIfAbsent won
            lock = other;
        }
    }
    return lock;
}

Java 8+:

ConcurrentMap<String, ReadWriteLock> lockMap = new ConcurrentHashMap<>();

ReadWriteLock getLock(String key) {
    return lockMap.computeIfAbsent(key, ReentrantReadWriteLock::new);
}

首先,像ConcurrentHashMap 这样的实现被记录为在read 操作上不使用锁。因此,在这种情况下,您打算为单个密钥获取锁的次数似乎比您打算创建新锁的次数多,这将减少线程争用。如果您使用synchronized,即使已经创建了锁,您也会强制每个线程通过临界区的单个文件。

此外,实现可以执行更高级的锁定形式,甚至是 shard 锁定,这样两个写入者就不必相互阻塞(如果写入底层数据结构的不同分区)。同样,synchronized 使用单个监视器对象,无法从了解底层数据结构的细节中受益。

得益于 lambda 和函数引用,Java 8 版本变成了单行版本。 ::new 语法是指相邻 ReentrantReadWriteLock 类的公共、无参数构造函数。 computeIfAbsent 方法只会在必要时调用该构造函数,并且基本上为您完成上述 Java 7 版本中的所有样板工作。如果创建新对象的成本很高或有不幸的副作用,这将特别有用。请注意,Java 7 版本必须在某些情况下创建新的锁实例,并且新对象可能不会被使用/返回。

【讨论】:

  • 同意。如果使用 Java 8,使用 computeIfAbsent() 更简单:ReadWriteLock lock = map.computeIfAbsent(key, ReentrantReadWriteLock::new)
【解决方案2】:

通常,您会使用同步来完成此操作。最简单的形式是同步方法本身。

public synchronized ReentrantReadWriteLock getLock(String tableName) {

但是,如果您担心性能,我会考虑以下使用同步块的方法,但前提是未找到初始锁。

public ReentrantReadWriteLock getLock(String tableName) {
    ReentrantReadWriteLock lock = locksMap.get(tableName);

    if (lock != null) {
        return lock;
    }

    synchronized(locksMap) {
        lock = locksMap.get(tableName);
        if (lock == null) {
            lock = new ReentrantReadWriteLock();
            locksMap.put(tableName, lock);
        }
    }
    return lock;
}

【讨论】:

  • 第二种方法不是线程安全的,因为JVM可以优化两条lock = locksMap.get(tableName)指令。
  • 您正在同时读取和写入 HashMap,这不是线程安全的。
  • 我同意 Map 对象的第一次读取不是线程安全的。但是,在同步块内的第二次读取确保一次只发生在一个线程中。我不知道 JVM 执行的任何优化都会与这种类型的编程发生冲突,但如果是这样的话,我很想听听更多关于它的信息:)。
  • @skimbleton 看看这个问题,它会给你一些见解:stackoverflow.com/questions/14624365/…
【解决方案3】:

您可以将方法修改为synchronized 或在方法中添加synchronization block,包括get()put() 调用。请注意,有一个关于此的伪模式(我更喜欢称其为习语),称为 Double-checked Locking

另一种选择是使用ConcurrentMap,它提供putIfAbsent() method

请注意,您会遇到很多关于各种选项的性能的讨论/辩论。我鼓励你带着健康的一粒盐来阅读它们。微优化和性能分析是危险的领域,代码的可读性和可维护性通常远远超过几微秒。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-10-29
    • 1970-01-01
    • 2017-07-23
    • 2022-12-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多