服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - Java 根据某个 key 加锁的实现方式

Java 根据某个 key 加锁的实现方式

2023-03-17 14:22明明如月学长 Java教程

日常开发中,有时候需要根据某个 key 加锁,确保多线程情况下,对该 key 的加锁和解锁之间的代码串行执行,这篇文章主要介绍了Java 根据某个 key 加锁的实现方式,需要的朋友可以参考下

一、背景

日常开发中,有时候需要根据某个 key 加锁,确保多线程情况下,对该 key 的加锁和解锁之间的代码串行执行。
大家可以借助每个 key 对应一个 ReentrantLock ,让同一个 key 的线程使用该 lock 加锁;每个 key 对应一个 Semaphore ,让同一个 key 的线程使用 Semaphore 控制同时执行的线程数。

二、参考代码

接口定义

?
1
2
3
4
5
6
7
8
9
10
11
12
public interface LockByKey<T> {
 
    /**
     * 加锁
     */
    void lock(T key);
 
    /**
     * 解锁
     */
    void unlock(T key);
}

2.1 同一个 key 只能一个线程执行

2.1.1 代码实现

每个 key 对应一个 ReentrantLock ,让同一个 key 的线程使用该 lock 加锁。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
 
public class DefaultLockByKeyImpl<T> implements LockByKey<T> {
 
    private final Map<T, ReentrantLock> lockMap = new ConcurrentHashMap<>();
 
    /**
     * 加锁
     */
    @Override
    public void lock(T key) {
        // 如果key为空,直接返回
        if (key == null) {
            throw new IllegalArgumentException("key 不能为空");
        }
        
        // 获取或创建一个ReentrantLock对象
        ReentrantLock lock = lockMap.computeIfAbsent(key, k -> new ReentrantLock());
        // 获取锁
        lock.lock();
    }
 
 
    /**
     * 解锁
     */
    @Override
    public void unlock(T key) {
        // 如果key为空,直接返回
        if (key == null) {
            throw new IllegalArgumentException("key 不能为空");
        }
 
        // 从Map中获取锁对象
        ReentrantLock lock = lockMap.get(key);
        // 获取不到报错
        if (lock == null) {
            throw new IllegalArgumentException("key " + key + "尚未加锁");
        }
        // 其他线程非法持有不允许释放
        if (!lock.isHeldByCurrentThread()) {
            throw new IllegalStateException("当前线程尚未持有,key:" + key + "的锁,不允许释放");
        }
        lock.unlock();
    }
}

注意事项:
(1)参数合法性校验
(2)解锁时需要判断该锁是否为当前线程持有

2.1.2 编写单测

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import com.google.common.collect.Lists;
import org.junit.Test;
 
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
 
public class DefaultLockByKeyImplTest {
 
    private final LockByKey<String> lockByKey = new DefaultLockByKeyImpl<>();
 
    private final CountDownLatch countDownLatch = new CountDownLatch(7);
    private final ExecutorService executorService = Executors.newFixedThreadPool(10);
 
    @Test
    public void test() throws InterruptedException {
        List<String> keys = Lists.newArrayList("a", "a", "a", "b", "c", "b", "d");
        Set<String> executingKeySet = new HashSet<>();
 
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            int finalI = i;
            executorService.submit(() -> {
                lockByKey.lock(key);
                if (executingKeySet.contains(key)) {
                    throw new RuntimeException("存在正在执行的 key:" + key);
                }
                executingKeySet.add(key);
 
                try {
                    System.out.println("index:" + finalI + "对 [" + key + "] 加锁 ->" + Thread.currentThread().getName());
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    System.out.println("index:" + finalI + "释放 [" + key + "] ->" + Thread.currentThread().getName());
                    lockByKey.unlock(key);
                    executingKeySet.remove(key);
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
    }
}

如果同一个 key 没释放能够再次进入,会抛出异常。
也可以通过日志来观察执行情况:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
index:0对 [a] 加锁 ->pool-1-thread-1
index:6对 [d] 加锁 ->pool-1-thread-7
index:4对 [c] 加锁 ->pool-1-thread-5
index:3对 [b] 加锁 ->pool-1-thread-4
index:6释放 [d] ->pool-1-thread-7
index:4释放 [c] ->pool-1-thread-5
index:0释放 [a] ->pool-1-thread-1
index:3释放 [b] ->pool-1-thread-4
 
index:1对 [a] 加锁 ->pool-1-thread-2
index:5对 [b] 加锁 ->pool-1-thread-6
index:1释放 [a] ->pool-1-thread-2
index:5释放 [b] ->pool-1-thread-6
 
index:2对 [a] 加锁 ->pool-1-thread-3
index:2释放 [a] ->pool-1-thread-3

2.2、同一个 key 可以有 n个线程执行

2.2.1 代码实现

每个 key 对应一个 Semaphore ,让同一个 key 的线程使用 Semaphore 控制同时执行的线程数。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import lombok.SneakyThrows;
 
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
 
public class SimultaneousEntriesLockByKey<T> implements LockByKey<T> {
 
    private final Map<T, Semaphore> semaphores = new ConcurrentHashMap<>();
 
    /**
     * 最大线程
     */
    private int allowed_threads;
 
    public SimultaneousEntriesLockByKey(int allowed_threads) {
        this.allowed_threads = allowed_threads;
    }
 
    /**
     * 加锁
     */
    @Override
    public void lock(T key) {
        Semaphore semaphore = semaphores.compute(key, (k, v) -> v == null ? new Semaphore(allowed_threads) : v);
        semaphore.acquireUninterruptibly();
    }
 
 
    /**
     * 解锁
     */
    @Override
    public void unlock(T key) {
        // 如果key为空,直接返回
        if (key == null) {
            throw new IllegalArgumentException("key 不能为空");
        }
 
        // 从Map中获取锁对象
        Semaphore semaphore = semaphores.get(key);
        if (semaphore == null) {
            throw new IllegalArgumentException("key " + key + "尚未加锁");
        }
        semaphore.release();
        if (semaphore.availablePermits() >= allowed_threads) {
            semaphores.remove(key, semaphore);
        }
    }

2.2.2 测试代码

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import com.google.common.collect.Lists;
import org.junit.Test;
 
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
 
public class SimultaneousEntriesLockByKeyTest {
 
    private final int maxThreadEachKey = 2;
    private final LockByKey<String> lockByKey = new SimultaneousEntriesLockByKey<>(maxThreadEachKey);
 
    private final CountDownLatch countDownLatch = new CountDownLatch(7);
    private final ExecutorService executorService = Executors.newFixedThreadPool(10);
 
    @Test
    public void test() throws InterruptedException {
        List<String> keys = Lists.newArrayList("a", "a", "a", "b", "c", "b", "d");
        Map<String, Integer> executingKeyCount = Collections.synchronizedMap(new HashMap<>());
 
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            int finalI = i;
            executorService.submit(() -> {
                lockByKey.lock(key);
                executingKeyCount.compute(key, (k, v) -> {
                    if (v != null && v + 1 > maxThreadEachKey) {
                        throw new RuntimeException("超过限制了");
                    }
                    return v == null ? 1 : v + 1;
                });
                try {
                    System.out.println("time:" + LocalDateTime.now().toString() + " ,index:" + finalI + "对 [" + key + "] 加锁 ->" + Thread.currentThread().getName() + "count:" + executingKeyCount.get(key));
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    System.out.println("time:" + LocalDateTime.now().toString() + " ,index:" + finalI + "释放 [" + key + "] ->" + Thread.currentThread().getName() + "count:" + (executingKeyCount.get(key) - 1));
                    lockByKey.unlock(key);
                    executingKeyCount.compute(key, (k, v) -> v - 1);
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
    }
}

输出:

time:2023-03-15T20:49:57.044195 ,index:6对 [d] 加锁 ->pool-1-thread-7count:1
time:2023-03-15T20:49:57.058942 ,index:5对 [b] 加锁 ->pool-1-thread-6count:2
time:2023-03-15T20:49:57.069789 ,index:1对 [a] 加锁 ->pool-1-thread-2count:2
time:2023-03-15T20:49:57.042402 ,index:4对 [c] 加锁 ->pool-1-thread-5count:1
time:2023-03-15T20:49:57.046866 ,index:0对 [a] 加锁 ->pool-1-thread-1count:2
time:2023-03-15T20:49:57.042991 ,index:3对 [b] 加锁 ->pool-1-thread-4count:2
time:2023-03-15T20:49:58.089557 ,index:0释放 [a] ->pool-1-thread-1count:1
time:2023-03-15T20:49:58.082679 ,index:6释放 [d] ->pool-1-thread-7count:0
time:2023-03-15T20:49:58.084579 ,index:4释放 [c] ->pool-1-thread-5count:0
time:2023-03-15T20:49:58.083462 ,index:5释放 [b] ->pool-1-thread-6count:1
time:2023-03-15T20:49:58.089576 ,index:3释放 [b] ->pool-1-thread-4count:1
time:2023-03-15T20:49:58.085359 ,index:1释放 [a] ->pool-1-thread-2count:1
time:2023-03-15T20:49:58.096912 ,index:2对 [a] 加锁 ->pool-1-thread-3count:1
time:2023-03-15T20:49:59.099935 ,index:2释放 [a] ->pool-1-thread-3count:0

三、总结

本文结合自己的理解和一些参考代码,给出自己的示例,希望对大家有帮助。

到此这篇关于Java 根据某个 key 加锁的实现方式的文章就介绍到这了,更多相关Java根据某个 key 加锁内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://blog.csdn.net/w605283073/article/details/129568863

延伸 · 阅读

精彩推荐
  • Java教程spring boot 命令行启动的方式

    spring boot 命令行启动的方式

    这篇文章主要介绍了spring boot 命令行启动的方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面...

    posuoren8172021-07-22
  • Java教程Spring MVC整合Shiro权限控制的方法

    Spring MVC整合Shiro权限控制的方法

    这篇文章主要介绍了Spring MVC整合Shiro权限控制,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    摆码王子5032021-04-26
  • Java教程Spring cloud Eureka注册中心搭建的方法

    Spring cloud Eureka注册中心搭建的方法

    这篇文章主要介绍了Spring cloud Eureka注册中心搭建的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    东北小狐狸10452021-04-22
  • Java教程Java中单例模式详解

    Java中单例模式详解

    这篇文章主要介绍了Java中单例模式详解,单例模式包括了懒汉式单例、饿汉式单例、登记式单例三种,想要了解的朋友可以了解一下。 ...

    会飞的小祥4132020-07-02
  • Java教程Spring整合mybatis实现过程详解

    Spring整合mybatis实现过程详解

    这篇文章主要介绍了Spring整合mybatis实现过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考...

    赫拉克利特3522020-07-16
  • Java教程全面解析Java中的HashMap类

    全面解析Java中的HashMap类

    HashMap类为Java提供了键值对应的map类型,本文将从源码角度全面解析Java中的HashMap类,同时包括其各种常用操作方法等,欢迎参考与借鉴 ...

    pastqing1512020-05-01
  • Java教程原来Java中有两个ArrayList

    原来Java中有两个ArrayList

    原来Java中有两个ArrayList,本文就带着大家一起探究Java中的ArrayList,感兴趣的小伙伴们可以参考一下 ...

    云聪4392020-03-21
  • Java教程springboot实现后台上传图片(工具类)

    springboot实现后台上传图片(工具类)

    这篇文章主要为大家详细介绍了springboot实现后台上传图片,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    睡觉觉啦9472021-09-10