本文主要是介绍java单体服务自定义锁名称工具类,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
需求:
操作员能够对自己权限下的用户数据进行数据填充,但是不同操作员之间可能会有重复的用户数据,为了避免操作员覆盖数据或者重复操作数据,应该在操作用户数据时加锁,要求加的这一把锁必须是细粒度的锁,不能锁住所有用户的操作 ,只锁当前操作用户,锁的名字为用户id,在获取到锁之后执行业务操作,否则阻塞等待, 这个单体服务锁工具如何设计?
思路:
思路:
- 1、锁工具类:首先创建一个ConcurrentHashMap{k(用户id)
v(锁实例)},包含获取锁方法,定时任务晚上删除map中键值对的方法(尝试获取锁,如果能够拿到锁就说明锁没有在使用) - 2、在定时任务删除某个键值对期间,去尝试获取写锁,如果能够获取到说明所以经不再被使用,直接删除map中的键值对,之后被删除的锁实例失效
- 3、其他线如果在删除锁实例之前获取到了,在执行业务的前后需要判断一下这把锁是否和当前key下的锁实例是否一致,一样执行业务,否则锁失效,抛异常,事务回滚
代码:
public class LockUtilsTest {public static final ConcurrentHashMap<String, ReentrantLock> lockMap = new ConcurrentHashMap<>();private static ScheduledExecutorService scheduledExecutorService;private static volatile boolean scheduledTaskCreated = false;/*** 获取锁实例,如果锁不存在就创建一个新的锁实例,并将这个锁实例返回* @param lockName* @return*/public static final ReentrantLock getLock(String lockName) {return lockMap.computeIfAbsent(lockName, key -> {//创建定时任务createdTask();return new ReentrantLock();});}/**** 懒加载 创建定时任务*/private static void createdTask(){System.out.println(Thread.currentThread().getName()+":尝试创建定时任务");if (!scheduledTaskCreated) {synchronized (LockUtilsTest.class) {if (!scheduledTaskCreated) {System.out.println(Thread.currentThread().getName()+":成功创建定时任务");scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();scheduledExecutorService.scheduleAtFixedRate(() -> {// 调用当前类的 clearUserLocks 方法new LockUtilsTest().clearUserLocks();}, 3, 3, TimeUnit.SECONDS);scheduledTaskCreated = true;}else{System.out.println(Thread.currentThread().getName()+":尝试创建定时任务发现定时任务已经被创建");}}}else{System.out.println(Thread.currentThread().getName()+":尝试创建定时任务发现定时任务已经被创建");}}/*** 遍历map,尝试获取锁,没获取到说明这个key的锁还在被使用,获取到就直接删除这个键值对,并且其他拥有这个被删除的锁实例的线程获取到锁也不执行业务*/private void clearUserLocks() {if (StringUtils.isNotEmpty(lockMap) && lockMap.size() >0){System.out.println(Thread.currentThread().getName()+":执行定时任务,删除未使用的锁");ArrayList<String> keys = new ArrayList<>();lockMap.forEach((key, value) -> {ReentrantLock lock = lockMap.get(key);//获取到锁之后直接删除键值对,否则锁在被使用不删除if (lock.tryLock()){try {keys.add(key);lockMap.remove(key);}finally {lock.unlock();lock = null;//取消强引用防止内存泄露}}});System.out.println(Thread.currentThread().getName()+":被删除的锁:"+keys.toString());}}/*** 极端情况下才会考虑这种删除锁时,可以使用分段锁思想,加入一百万个用户,我按照一个用户一把锁,估计会创建一百万把,但是使用分段锁之后,可以让一万个用户公用一把锁,* 这样就会减少锁的数量,还有频繁加锁和释放锁带来的开销,相比与百万数据公用一把锁改成万人用一把锁,性能是有明显提升的,锁的垃圾回收也要处理好,不要出现内存泄露*//**** 判断锁是否有效 :当定时任务删除对应键的锁时,如果有线程已经获得这个键原来的锁实例没抢到锁时,恰好另一个线程在清空键值对后获取这个key的锁实例,这个key在这一瞬间就会存在两个锁实例* 他们可以同时执行业务,彼此并不互斥,所以当获得锁之后,要再次和当前锁是否一致,只要不一致就说明这个锁实例不能再使用了,会存在线程不安全问题* @param lockName*/public static final Boolean checkLock(String lockName,ReentrantLock checkLock){ReentrantLock nowLock = lockMap.get(lockName);/**确保锁存在并且 比较两个对象是否是一个实例*/if (ObjectUtil.isNotEmpty(nowLock) && checkLock == nowLock){return true;}return false;}//使用这个方法如果有数据库操作,要加事务注解回滚数据public static final void checkLockFlag(String lockName,ReentrantLock checkLock){ReentrantLock nowLock = lockMap.get(lockName);/**确保锁存在并且 比较两个对象是否是一个实例*/if (ObjectUtil.isNotEmpty(nowLock) && checkLock == nowLock){}else{throw new RuntimeException("当前锁已失效");}}
}class LockUtilsTestClass{public static void main(String[] args) {//测试定时任务是否是单例懒加载创建 =》 目标 : 成功创建定时任务//taskTest();//测试一百个线程抢锁,锁阻塞是否生效 =》 目标 :执行业务出现一百次 后缀是同一把锁//lockTest();//测试锁被删除后,已经获取锁实例的线程是否生效 =》 目标:老锁返回友好提示 五次 √delLockTest();}/*** 测试定时任务是否是单例懒加载创建* 预期结果:* 尝试创建定时任务 100次* 尝试创建定时任务发现定时任务已经被创建 99次* 成功创建定时任务 1次*/static void taskTest(){for (int i = 0; i < 100; i++) {new Thread(() ->{ReentrantLock testLock = LockUtilsTest.getLock("test");}).start();}}/**** 测试锁阻塞是否生效 =》 目标 同一个锁执行业务一百次*/static void lockTest(){for (int i = 0; i < 100; i++) {new Thread(() ->{String lockName = "fe2r32r23f34t34tf34f34fd";ReentrantLock lock = LockUtilsTest.getLock(lockName);lock.lock();try {System.out.println("执行业务"+lock.toString());}finally {lock.unlock();}}).start();}}static void delLockTest(){for (int i = 0; i < 5; i++) {new Thread(() ->{String lockName = "fe2r32r23f34t34tf34f34fd";ReentrantLock lock = LockUtilsTest.getLock(lockName);try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}lock.lock();try {if (LockUtilsTest.checkLock(lockName,lock)){//执行业务System.out.println("老锁执行业务");}else{//当前线程持有锁失效,返回友好提示System.out.println("老锁返回友好提示");}}catch (Exception e){e.printStackTrace();}finally {lock.unlock();}}).start();}}
}
工具类优缺点:
-
使用ConcurrentHashMap作为锁的存储容器有以下优点:
-
线程安全:ConcurrentHashMap本身是线程安全的,多个线程可以并发地访问和修改它,而无需额外的同步。
-
高效性:ConcurrentHashMap在并发场景下通常比synchronizedMap或Hashtable等同步容器有更好的性能。
-
灵活性:你可以根据键(例如资源的唯一标识)动态地获取和释放锁,这使得代码更加灵活和可维护。
-
重用锁对象:尽可能地重用现有的锁对象,而不是为每个新的资源或操作都创建一个新的锁,如果你频繁地创建和销毁锁对象,或者维护大量的锁对象(如存储在ConcurrentHashMap中),那么这确实会增加内存消耗。每个ReentrantLock对象都会占用一定的内存空间,而且如果锁的数量非常大,那么存储这些锁的数据结构(如ConcurrentHashMap)也会占用更多的内存,当你动态地获取锁时,JVM会确保线程安全地访问锁对象,这通常涉及一些底层的同步机制,但这些机制通常不会引入大量的额外内存消耗。同样,释放锁也只是改变锁的状态,使其可以被其他线程获取,这同样是一个轻量级的操作。
-
注意问题
-
避免无限制地增长:虽然将锁存储在ConcurrentHashMap中可以限制锁对象的数量,但如果你的应用程序允许无限制地添加新的键(即资源),那么lockMap本身可能会无限制地增长,从而消耗大量内存。确保你的应用程序有适当的机制来管理资源,避免无限制地添加新的锁。
这篇关于java单体服务自定义锁名称工具类的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!