Java 中通过 key 获取锁的方法

 更新时间:2022年11月15日 09:33:08   作者:明明如月学长  
这篇文章主要介绍了Java 中通过 key 获取锁,本文演示如何对某个 key 加锁,以保证对该 key 的并发操作限制,可以实现同一个 key 一个或者多个线程同时执行,需要的朋友可以参考下

一、概览

本文我们将了解如何通过特定键获取锁,以保证该键上的操作的线程安全,并且不妨碍其他键。
一般来说,我们需要实现两个方法:

void lock(String key)
void unlock(String key)

本文以字符串作为键为例,大家可以根据实际需要改造成任意类型的键,重写 equas 和 hashCode 方法,保证唯一性即可。

二、简单的互斥锁

假设需要满足当前线程获取锁则需要执行特定代码,否则不执行这个场景。
我们可以维护一系列 Key 的 Set, 在使用时添加到 Set 中,解锁时移除对应的 Key。
此时,需要考虑线程安全问题。因此需要使用线程安全的 Set 实现,如基于 ConcurrentHashMap 的线程安全 Set。

public class SimpleExclusiveLockByKey {

    private static Set<String> usedKeys= ConcurrentHashMap.newKeySet();
    
    public boolean tryLock(String key) {
        return usedKeys.add(key);
    }
    
    public void unlock(String key) {
        usedKeys.remove(key);
    }

}

使用案例:

String key = "key";
SimpleExclusiveLockByKey lockByKey = new SimpleExclusiveLockByKey();
try {
    lockByKey.tryLock(key);
    // 在这里添加对该 key 获取锁之后要执行的代码
} finally { // 非常关键
    lockByKey.unlock(key);
}

注意一定要在 finally 代码块中解锁,以保证即便发生异常时,也可以正常解锁。

三、按键来获取和释放锁

以上代码可以保证获取锁后才执行,但无法实现未拿到锁的线程等待的效果。
有时候,我们需要让未获取到对应锁的线程等待。
流程如下:

  • 第一个线程获取某个 key 的锁
  • 第二个线程获取同一个 key 的锁,第二个线程需要等待
  • 第一个线程释放某个 key 的锁
  • 第二个线程获取该 key 的锁,然后执行其代码

3.1 使用线程计数器定义 Lock

我们可以使用 ReentrantLock 来实行线程阻塞。
我们通过内部类来封装 Lock。该类统计某个 key 上执行的线程数。暴露两个方法,一个是线程数增加,一个是减少线程数。

private static class LockWrapper {
    private final Lock lock = new ReentrantLock();
    private final AtomicInteger numberOfThreadsInQueue = new AtomicInteger(1);

    private LockWrapper addThreadInQueue() {
        numberOfThreadsInQueue.incrementAndGet(); 
        return this;
    }

    private int removeThreadFromQueue() {
        return numberOfThreadsInQueue.decrementAndGet(); 
    }

}

3.2 处理排队的线程

接下来继续使用 ConcurrentHashMap , key 作为键, LockWrapper 作为值。
保证同一个 key 使用同一个 LockWrapper 中的同一把锁。

private static ConcurrentHashMap<String, LockWrapper> locks = new ConcurrentHashMap<String, LockWrapper>();

一个线程想要获取某个 key 的锁时,需要看该 key 对应的 LockWrapper 是否已经存在。

  • 如果不存在,创建一个 LockWrapper ,计数器设置为1
  • 如果存在,对应的 LockWrapper 加1
public void lock(String key) {
    LockWrapper lockWrapper = locks.compute(key, (k, v) -> v == null ? new LockWrapper() : v.addThreadInQueue());
    lockWrapper.lock.lock();
}

3.3 解锁和移除 Entry

解锁时将等待的队列减一。
当前 key 对应的线程数为 0 时,可以将其从 ConcurrentHashMap 中移除。

public void unlock(String key) {
    LockWrapper lockWrapper = locks.get(key);
    lockWrapper.lock.unlock();
    if (lockWrapper.removeThreadFromQueue() == 0) { 
        // NB : We pass in the specific value to remove to handle the case where another thread would queue right before the removal
        locks.remove(key, lockWrapper);
    }
}

3.4 总结

最终效果如下:

public class LockByKey {
    
    private static class LockWrapper {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger numberOfThreadsInQueue = new AtomicInteger(1);
        
        private LockWrapper addThreadInQueue() {
            numberOfThreadsInQueue.incrementAndGet(); 
            return this;
        }
        
        private int removeThreadFromQueue() {
            return numberOfThreadsInQueue.decrementAndGet(); 
        }
        
    }
    
    private static ConcurrentHashMap<String, LockWrapper> locks = new ConcurrentHashMap<String, LockWrapper>();
    
    public void lock(String key) {
        LockWrapper lockWrapper = locks.compute(key, (k, v) -> v == null ? new LockWrapper() : v.addThreadInQueue());
        lockWrapper.lock.lock();
    }
    
    public void unlock(String key) {
        LockWrapper lockWrapper = locks.get(key);
        lockWrapper.lock.unlock();
        if (lockWrapper.removeThreadFromQueue() == 0) { 
            // NB : We pass in the specific value to remove to handle the case where another thread would queue right before the removal
            locks.remove(key, lockWrapper);
        }
    }
}

使用示例:

String key = "key"; 
LockByKey lockByKey = new LockByKey(); 
try { 
    lockByKey.lock(key);
    // insert your code here 
} finally { // CRUCIAL 
    lockByKey.unlock(key); 
}

四、允许同一个 key 同时多个线程运行

我们还需要考虑另外一种场景: 前面对于同一个 key 同一时刻只允许一个线程执行。如果我们想实现,对于同一个 key ,允许同时运行 n 个线程该怎么办?
为了方便理解,我们假设同一个 key 允许两个线程。

  • 第一个线程想要获取 某个 key 的锁,允许
  • 第二个线程也想要获取该 key 的锁,允许
  • 第三个线程也想获取该 key 的锁,该线程需要等待第一个或第二个线程释放锁之后才可以执行

Semaphore 很适合这种场景。Semaphore 可以控制同时运行的线程数。

public class SimultaneousEntriesLockByKey {

    private static final int ALLOWED_THREADS = 2;
    
    private static ConcurrentHashMap<String, Semaphore> semaphores = new ConcurrentHashMap<String, Semaphore>();
    
    public void lock(String key) {
        Semaphore semaphore = semaphores.compute(key, (k, v) -> v == null ? new Semaphore(ALLOWED_THREADS) : v);
        semaphore.acquireUninterruptibly();
    }
    
    public void unlock(String key) {
        Semaphore semaphore = semaphores.get(key);
        semaphore.release();
        if (semaphore.availablePermits() == ALLOWED_THREADS) { 
            semaphores.remove(key, semaphore);
        }  
    }
    
}

使用案例:

String key = "key"; 
SimultaneousEntriesLockByKey lockByKey = new SimultaneousEntriesLockByKey(); 
try { 
    lockByKey.lock(key); 
    // 在这里添加对该 key 获取锁之后要执行的代码
} finally { // 非常关键
    lockByKey.unlock(key); 
}

五、结论

本文演示如何对某个 key 加锁,以保证对该 key 的并发操作限制,可以实现同一个 key 一个或者多个线程同时执行。
相关代码:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-concurrency-advanced-4

原文:https://www.baeldung.com/java-acquire-lock-by-key

到此这篇关于Java 中通过 key 获取锁的文章就介绍到这了,更多相关java 获取锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java使用递归回溯完美解决八皇后的问题

    Java使用递归回溯完美解决八皇后的问题

    这篇文章主要介绍了Java基于循环递归回溯实现八皇后问题算法,结合具体实例形式分析了java的遍历、递归、回溯等算法实现八皇后问题的具体步骤与相关操作技巧,需要的朋友可以参考下
    2021-11-11
  • 浅谈controller中调用多个service方法的问题

    浅谈controller中调用多个service方法的问题

    这篇文章主要介绍了浅谈controller中调用多个service方法的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • Java几种常用JSON库性能比较详解

    Java几种常用JSON库性能比较详解

    这篇文章主要介绍了Java几种常用JSON库性能比较详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-06-06
  • Java中的ThreadLocal详解

    Java中的ThreadLocal详解

    这篇文章主要介绍了Java中的ThreadLocal详解,ThreadLocal 是一个线程局部变量,其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,需要的朋友可以参考下
    2023-09-09
  • 最详细的Java循环结构解析之for循环教程(适合小白)

    最详细的Java循环结构解析之for循环教程(适合小白)

    :循环结构是指在程序中需要反复执行某个功能而设置的一种程序结构,下面这篇文章主要给大家介绍了关于Java循环结构解析之for循环的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2021-09-09
  • java实现装饰器模式(Decorator Pattern)

    java实现装饰器模式(Decorator Pattern)

    这篇文章主要为大家详细介绍了java实现装饰器模式Decorator Pattern,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-10-10
  • Spring boot 整合 Redisson实现分布式锁并验证功能

    Spring boot 整合 Redisson实现分布式锁并验证功能

    这篇文章主要介绍了Spring boot 整合 Redisson实现分布式锁并验证,redisson是官方推荐的分布式锁实现方案,采用redis自身的原子命令和lua脚本来实现,需要的朋友可以参考下
    2022-05-05
  • java数组遍历 删除remove(示例代码)

    java数组遍历 删除remove(示例代码)

    java数组遍历 删除remove。需要的朋友可以过来参考下,希望对大家有所帮助
    2013-10-10
  • springMVC在restful风格的性能优化方案

    springMVC在restful风格的性能优化方案

    这篇文章主要介绍了springMVC在restful风格的性能优化方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • 详解Java集合类之HashSet篇

    详解Java集合类之HashSet篇

    这篇文章主要为大家详细介绍一下Java集合类中HashSet的用法,文中的示例代码讲解详细,对我们学习Java有一定帮助,感兴趣的可以了解一下
    2022-07-07

最新评论