当前位置: 首页 > 知识库问答 >
问题:

任意键的锁定处理程序

裘嘉树
2023-03-14

我的代码实现了一个任意键的“锁处理程序”。给定key,它确保每次只有一个线程可以process该(或等于)key(这里表示调用ExternalSystem.process(key)调用)。

到目前为止,我有这样的代码:

public class MyHandler {
    private final SomeWorkExecutor someWorkExecutor;
    private final ConcurrentHashMap<Key, Lock> lockMap = new ConcurrentHashMap<>();

    public void handle(Key key) {
        // This can lead to OOM as it creates locks without removing them
        Lock keyLock = lockMap.computeIfAbsent( 
            key, (k) -> new ReentrantLock()
        );
        keyLock.lock();
        try {
            someWorkExecutor.process(key);
        } finally {
            keyLock.unlock();
        }
    }
}

我理解这段代码可能导致OutOfMemoryError,因为没有一个清晰的映射。

我重新阅读了该任务,现在我发现我的限制是handle方法不能被调用超过8个线程。我不知道它对我有什么帮助,但我刚刚提到了。

P.S.2

@蜘蛛鲍里斯建议了一个很好又简单的解决方案:

} finally {
      lockMap.remove(key);
      keyLock.unlock();
}

但在Boris注意到我们的代码不是线程安全的,因为它破坏了行为:
让research 3个线程以相同的键调用

  1. 线程#1获取锁,现在在map.remove(key)之前;
  2. 线程#2使用equals键调用,以便在线程#1释放锁时等待。
  3. 然后线程#1执行map.remove(key);。在此之后,线程#3调用方法handle。它检查这个键的锁在map中不存在,从而创建新的锁并获取它。
  4. 线程#1释放锁,因此线程#2获取锁。
    因此线程#2和线程#3可以并行调用equals键。但不应允许。

为了避免这种情况,在清除映射之前,我们应该阻止任何线程获取锁,而waitset中的所有线程都没有获取并释放锁。看来这是足够复杂的同步需要,这将导致算法工作缓慢。也许当地图大小超过某个限定值时,我们应该不时清除地图。

我浪费了很多时间,但不幸的是,我不知道如何实现这一点。

共有1个答案

卫阳曜
2023-03-14

您不需要尝试将大小限制为某个任意值--事实证明,您可以完成这种“锁处理程序”习惯用法,同时只存储映射中当前锁定的键的确切数量。

其思想是使用一个简单的约定:将映射成功添加到映射中算作“锁定”操作,将其移除算作“解锁”操作。这很好地避免了在某些线程仍然锁定映射和其他竞争条件时删除映射的问题。

此时,映射中的value仅用于阻止使用相同密钥到达并需要等待直到映射被移除的其他线程。

下面是一个1示例,其中CountDownLatch而不是Lock作为映射值:

public void handle(Key key) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(1);

    // try to acquire the lock by inserting our latch as a
    // mapping for key        
    while(true) {
        CountDownLatch existing = lockMap.putIfAbsent(key, latch);
        if (existing != null) {
            // there is an existing key, wait on it
            existing.await();
        } else {
            break;
        }
    }

    try {
        externalSystem.process(key);
    } finally {
        lockMap.remove(key);
        latch.countDown();
    }
}

在这里,映射的生存期只有锁被持有的时间。映射的条目永远不会超过对不同密钥的并发请求。

与您的方法的不同之处在于映射没有被“重用”-每个handle调用都将创建一个新的锁存器和映射。由于您已经在进行昂贵的原子操作,这在实践中不太可能是一个很大的减速。另一个缺点是,对于许多等待的线程,当锁存器倒计时时,所有线程都被唤醒,但只有一个线程将成功地将新映射放入并因此获得锁-其余的线程将返回到新锁上Hibernate。

您可以构建另一个版本,当线程出现并等待现有映射时,重新使用映射。基本上,解锁线程只是对其中一个等待线程进行“切换”。只有一个映射将用于等待同一键的一整套线程--它按顺序传递给每一个线程。大小仍然是有界的,因为没有更多的线程等待给定的映射,它仍然被移除。

要实现这一点,您将CountDownLatch替换为一个可以计算等待线程数的映射值。当一个线程执行解锁时,它首先检查是否有线程正在等待,如果有,则唤醒一个线程执行切换。如果没有线程在等待,它会“销毁”对象(即,设置一个标志,表明对象不再在映射中),并将其从映射中移除。

您需要在一个适当的锁下进行上述操作,还有一些棘手的细节。在实践中,我发现上面的简短而甜蜜的例子很管用。

1html" target="_blank">动态编写的,没有编译,也没有测试,但这个想法是可行的。

 类似资料:
  • 问题内容: 我有为任意键实现“锁定处理程序”的代码。给定一个,它确保一次只能有一个线程可以(或等于)该键(这意味着调用该调用)。 到目前为止,我有这样的代码: 我知道这段代码可能导致,因为没有清晰的地图。 我考虑如何制作地图,该地图将累积有限数量的元素。当超过限制时,我们应该用new替换最旧的访问元素(此代码应与最旧的元素作为监视器同步)。但是我不知道如何进行回调,这将告诉我超出限制。 请分享您的

  • 我正在使用sping-xd通过批处理作业进行数据摄取。大量作业在4个容器中并行运行。任何地方都在10到40个作业之间。其中大多数在不到一分钟的时间内完成。我使用redis(而不是Rabbitmq)和mysql进行数据存储。Spring-xd-批处理使用不同的mysql-db进行作业/步骤统计,我的应用程序使用不同的mysql-db用于自己的目的。两个mysql-db都在同一台服务器上。所有4个容器

  • 在我的模型中,我有9个不同的服务块,每个服务可以产生9个不同的特性。每种组合都有不同的延迟时间和标准差。例如,特征3在服务块8中需要5分钟的偏差为0.05,但在服务块4中只需要3分钟的偏差为0.1。 我如何永久跟踪每个组合的最后5个需要的次数,并计算平均值(像一个移动平均线)?我想用平均值来让产品根据最短的时间来决定为各自的功能选择哪一个服务块,比较所有机器为各自的功能所做的过去时间。产品代理已经

  • 死锁描述了两个或多个线程永远被阻塞,等待彼此的情况。 当多个线程需要相同的锁但以不同的顺序获取它们时,会发生死锁。 Java多线程程序可能会遇到死锁条件,因为synchronized关键字会导致执行线程在等待与指定对象关联的锁定或监视器时阻塞。 这是一个例子。 例子 (Example) public class TestThread { public static Object Lock1

  • 主要内容:1.死锁无知,2.死锁预防,3.避免死锁,4.死锁检测和恢复1.死锁无知 死锁无知是所有机制中使用最广泛的方法。 许多操作系统主要为最终用户使用。 在这种方法中,操作系统假定永远不会发生死锁。 它只是无视死锁。 这种方法最适合用户使用系统仅用于浏览和所有其他常规内容的单个最终用户系统。 正确性和性能之间总是有一个折衷。 Windows和Linux等操作系统主要关注性能。 但是,如果死锁发生的次数是100次,那么系统的性能就会一直下降,如果它始终使用死锁处理

  • 我有一个类,其中一个方法运行了X分钟,另一个方法调用了一个事件处理程序。这两个都会修改静态列表的状态。 下面是代码 事件处理器方法将检查进程,如果进程存在,它将执行一些逻辑,如果进程不存在,它将使用新的逻辑更新相同的进程。 计时器将每隔X间隔运行一次,并将每个进程的当前状态发送到外部系统,并将从中删除该进程。 在这里,我需要确保中的检查代码与 中的删除代码 不冲突 根据我的理解,添加一个锁会阻止使