当前位置: 首页 > 面试题库 >

任意键的锁定处理程序

淳于慎之
2023-03-14
问题内容

我有为任意键实现“锁定处理程序”的代码。给定一个key,它确保一次只能有一个线程可以process(或等于)该键(这意味着调用该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因为没有清晰的地图。

我考虑如何制作地图,该地图将累积有限数量的元素。当超过限制时,我们应该用new替换最旧的访问元素(此代码应与最旧的元素作为监视器同步)。但是我不知道如何进行回调,这将告诉我超出限制。

请分享您的想法。

聚苯乙烯

我重新阅读了任务,现在我发现我的局限性是handle不能调用8个以上线程的方法。我不知道这对我有什么帮助,但我刚才提到了。

PS2

通过@Boris提出了Spider的一种很好而简单的解决方案

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

但是在鲍里斯(Boris)注意到我们的代码不安全之后,我们就认为它不是线程安全的,因为它破坏了行为:
让研究3个具有相同键的线程被调用:

  1. 线程#1获取了锁,现在之前 map.remove(key);
  2. 线程#2使用等号调用,因此它在线程#1释放锁时等待。
  3. 然后执行线程#1 map.remove(key);。在此线程#3之后调用method handle。它检查映射中是否缺少该密钥的锁,因此它将创建新的锁并获取它。
  4. 线程1释放了锁,因此线程2获得了该锁。
    因此,可以为equals键并行调用线程#2和线程#3。但这是不允许的。

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

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


问题答案:

您无需尝试将大小限制为任意值-事实证明,您可以完成这种“锁定处理程序”惯用语,而仅存储当前在映射中锁定的键的 确切 数目。

这个想法是使用一个简单的约定:成功地 映射 添加
到映射计数为“锁定”操作,而将其删除则作为“解锁”操作。巧妙地避免了在某些线程仍处于锁定状态和其他竞争条件的情况下删除映射的问题。

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

这是一个示例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调用都会创建一个新的闩锁和映射。由于您已经在进行昂贵的原子操作,因此这实际上不太可能会变慢。另一个缺点是,在有许多等待线程的情况下,当闩锁递减计数时,
所有 线程 都会 被唤醒,但是只有一个线程能够成功放入新的映射并因此获得锁-其余的则重新进入新锁的睡眠状态。

可以
构建此版本的另一个版本,该版本在线程进入并等待现有映射时重新使用映射。基本上,解锁线程只是对等待线程之一进行“切换”。只有一个映射将用于等待相同键的整个线程集-
它按顺序移交给每个线程。该大小仍受限制,因为没有更多线程在等待给定的映射,因此仍将其删除。

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

您需要在适当的锁定下进行上述操作,并且有一些棘手的细节。在实践中,我发现上面简短而甜美的示例非常有用。

1即时编写,未经编译且未经测试,但该想法有效。



 类似资料:
  • 我的代码实现了一个任意键的“锁处理程序”。给定,它确保每次只有一个线程可以该(或等于)key(这里表示调用调用)。 到目前为止,我有这样的代码: 我理解这段代码可能导致,因为没有一个清晰的映射。 我重新阅读了该任务,现在我发现我的限制是方法不能被调用超过8个线程。我不知道它对我有什么帮助,但我刚刚提到了。 P.S.2 @蜘蛛鲍里斯建议了一个很好又简单的解决方案: 但在Boris注意到我们的代码不是

  • 我正在使用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间隔运行一次,并将每个进程的当前状态发送到外部系统,并将从中删除该进程。 在这里,我需要确保中的检查代码与 中的删除代码 不冲突 根据我的理解,添加一个锁会阻止使