【问题标题】:Concurrent byte array access in Java with as few locks as possibleJava 中的并发字节数组访问,使用尽可能少的锁
【发布时间】:2017-02-02 03:42:03
【问题描述】:

我正在尝试减少分段数据的锁定对象的内存使用量。请参阅我的问题herehere。或者只是假设您有一个字节数组,并且每 16 个字节可以(反)序列化为一个对象。让我们称其为行长为 16 字节的“行”。现在,如果您从写入线程修改这样的行并从多个线程读取,则需要锁定。如果您的字节数组大小为 1MB (1024*1024),这意味着 65536 行和相同数量的锁。

这有点太多了,而且我需要更大的字节数组,我想将它减少到与线程数大致成比例的东西。我的想法是创建一个

ConcurrentHashMap<Integer, LockHelper> concurrentMap;

其中Integer 是行索引,在线程“进入”一行之前,它会在此映射中放置一个锁定对象(从this answer 得到这个想法)。但无论我怎么想,我都找不到真正线程安全的方法:

// somewhere else where we need to write or read the row
LockHelper lock1 = new LockHelper();
LockHelper lock = concurrentMap.putIfAbsent(rowIndex, lock1);
lock.addWaitingThread(); // is too late
synchronized(lock) {
  try { 
      // read or write row at rowIndex e.g. writing like
      bytes[rowIndex/16] = 1;
      bytes[rowIndex/16 + 1] = 2;
      // ...
  } finally {
     if(lock.noThreadsWaiting())
        concurrentMap.remove(rowIndex);
  }
}

你认为有可能使这个线程安全吗?

我觉得这看起来很像 concurrentMap.compute 构造(例如,参见 this answer),或者我什至可以使用这种方法吗?

map.compute(rowIndex, (key, value) -> {
    if(value == null)
       value = new Object();
    synchronized (value) {
        // access row
        return value;
    }
});
map.remove(rowIndex);

我们已经知道计算操作是原子的,因此值和“同步”是否必要?

// null is forbidden so use the key also as the value to avoid creating additional objects
ConcurrentHashMap<Integer, Integer> map = ...;

// now the row access looks really simple:
map.compute(rowIndex, (key, value) -> {
    // access row
    return key;
});
map.remove(rowIndex);

顺便说一句:自从我们在 Java 中使用此计算时。从1.8开始?在 JavaDocs 中找不到这个

更新:我发现了一个非常相似的问题here 使用 userIds 而不是 rowIndices,请注意,该问题包含一个示例,其中包含多个问题,例如缺少 final,在 @987654340 中调用 lock @-clause 并且没有缩小地图。似乎也有 a library JKeyLockManager 用于此目的,但 I don't think it is thread-safe

更新 2:解决方案似乎非常简单,因为 Nicolas Filotto 指出了如何避免删除:

map.compute(rowIndex, (key, value) -> {
    // access row
    return null;
});

所以这确实是内存密集度较低,但synchronized 的简单段锁定在my scenario 中至少快 50%。

【问题讨论】:

  • 也许您可以使用类似于 n-way-associative 缓存的排列方式?双向关联意味着一个锁用于偶地址行,一个锁用于奇地址行。根据线程的数量,您可以尝试使用锁的数量。如果访问中存在模式,则使用质数锁甚至原始哈希将行地址映射到匹配的锁对象可能会有所帮助。
  • 条带化假定分布良好,但由于执行少量工作而遭受争用和锁抖动。而是考虑一种消息传递方法,以拥有一个编写器(或每个核心一个段)。然后,您可以使用破坏者或扁平组合方法,通过执行一批工作而不是遭受争用来惩罚调用。
  • @GeorgBisseling 那是我们已经测试过的,请参阅源的更新。
  • @BenManes 你有一些链接可以更详细地解释你的方法吗?就我而言,我可以假设我只有一位作家,所以这会很有趣。
  • @Karussell 如果您不需要请求线程等待响应,这些技术效果最好。请参阅disruptorflat combiningcaffeine,了解记录 => 批处理 => 锁定 => 重放的不同变化。

标签: java multithreading concurrency locking


【解决方案1】:

值和synchronized 是必需的吗? 知道计算操作是原子的吗?

我确认在这种情况下不需要添加synchronized 块,因为compute 方法是原子完成的,如ConcurrentHashMap#compute(K key, BiFunction&lt;? super K,? super V,? extends V&gt; remappingFunction) 的Javadoc 中所述,自Java 8 以来已添加BiFunction ,我引用:

尝试计算指定键及其当前键的映射 映射值(如果没有当前映射,则为 null)。 整个 方法调用以原子方式执行。一些尝试更新 其他线程在此映射上的操作可能会被阻塞,而 计算正在进行中,所以计算应该很短并且 简单,并且不得尝试更新此 Map 的任何其他映射。

如果您让 BiFunction 始终返回 null 以原子方式删除密钥,那么您尝试使用 compute 方法实现的目标可能是完全原子的,这样一切都将原子地完成。

map.compute(
    rowIndex, 
    (key, value) -> {
        // access row here
        return null;
    }
);

这样,您将完全依赖 ConcurrentHashMap 的锁定机制来同步您对行的访问。

【讨论】:

  • 啊,所以null可以用来避免存储。感谢您指出了这一点!关于改进: ConcurrentHashMap 只是与线程数成比例地“存储”锁,因此数组可能在几十个条目的范围内,而我的方法每行需要一个锁,并且将是几百万(或更少如果范围明智)但总是比这种方法更多。
  • 使用ConcurrentHashMap,每个段都有一个锁,它覆盖了一大块哈希码,这也意味着在你的情况下你不会有一大块键或更确切地说是一大块行如您所料,每行一个锁。默认情况下,您将有 16 个段,如果您需要更多段,则需要使用更高的 concurrencyLevel 构建您的 ConcurrentHashMap
  • 啊,理解和使用 concurrencyLevel 正是我所需要的,可以将其设置为线程数。而且内存效率比我的解决方案高得多。
  • 啊,内存效率更高,有趣我敢打赌相反,反正好消息
  • 我猜这是因为您付出了代价,因为您不直接执行代码,而是通过 lambda 表达式执行代码。但对我来说,你的 OL 肯定是最好的,通常它是用钥匙上的模数来获得正确的锁,但想法是一样的
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-27
相关资源
最近更新 更多