赞
踩
ReentrantLock(可重入锁),指的是一个线程再次对已持有的锁保护的临界资源时,重入请求将会成功。
简单的与我们常用的Synchronized进行比较:
ReentrantLock | Synchronized | |
---|---|---|
锁实现机制 | 依赖AQS | 监视器模式 |
灵活性 | 支持响应超时、中断、尝试获取锁 | 不灵活 |
释放形式 | 必须显示调用unlock()释放锁 | 自动释放监视器 |
锁类型 | 公平锁 & 非公平锁 | 非公平锁 |
条件队列 | 可关联多个条件队列 | 关联一个条件队列 |
可重入性 | 可重入 | 可重入 |
AQS机制:如果被请求的共享资源空闲,那么就当前请求资源的线程设置为有效的工作线程,将共享资源通过CAScompareAndSetState
设置为锁定状态;如果共享资源被占用,就采用一定的阻塞等待唤醒机制(CLH变体的FIFO双端队列)来保证锁分配。
可重入性:无论是公平锁还是非公平锁的情况,加锁过程会利用一个state值
private volatile int state
- public class LockExample {
-
- static int count = 0;
- static ReentrantLock lock = new ReentrantLock();
-
- public static void main(String[] args) throws InterruptedException {
-
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
-
- try {
- // 加锁
- lock.lock();
- for (int i = 0; i < 10000; i++) {
- count++;
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- finally {
- // 解锁,放在finally子句中,保证锁的释放
- lock.unlock();
- }
- }
- };
-
- Thread thread1 = new Thread(runnable);
- Thread thread2 = new Thread(runnable);
- thread1.start();
- thread2.start();
- thread1.join();
- thread2.join();
- System.out.println("count: " + count);
- }
- }
-
- /**
- * 输出
- * count: 20000
- */

乐观锁即是无锁思想,一般都是基于CAS思想实现的,而在MySQL中通过version版本号 + CAS无锁形式实现乐观锁;例如T1,T2两个事务一起并发执行时,当T2事务执行成功提交后,会对version+1,所以T1事务执行的version条件就无法成立了。
对sql语句进行加锁以及状态机的操作,也可以避免不同线程同时对count值访问导致的数据不一致问题。
- // 乐观锁 + 状态机
- update
- table_name
- set
- version = version + 1,
- count = count + 1
- where
- id = id AND version = version AND count = [修改前的count值];
-
- // 行锁 + 状态机
- update
- table_name
- set
- count = count + 1
- where
- id = id AND count = [修改前的count值]
- for update;

如果我们直接采用ReentrantLock全局加锁,那么这种情况是一条线程获取到锁,整个程序全部的线程来到这里都会阻塞;但是我们在项目里面想要针对每个用户在操作的时候实现互斥逻辑,所以我们需要更加细粒度的锁。
- public class LockExample {
- private static Map<String, Lock> lockMap = new ConcurrentHashMap<>();
-
- public static void lock(String userId) {
- // Map中添加细粒度的锁资源
- lockMap.putIfAbsent(userId, new ReentrantLock());
- // 从容器中拿锁并实现加锁
- lockMap.get(userId).lock();
- }
- public static void unlock(String userId) {
- // 先从容器中拿锁,确保锁的存在
- Lock locak = lockMap.get(userId);
- // 释放锁
- lock.unlock();
- }
- }

弊端:如果每一个用户请求共享资源,就会加锁一次,后续该用户就没有在登录过平台,但是锁对象会一直存在于内存中,这等价于发生了内存泄漏,所以锁的超时和淘汰机制机制需要实现。
上面的加锁机制使用到了锁容器ConcurrentHashMap
,该容易为了线程安全的情况,多以底层还是会用到Synchronized
机制,所以有些情况,使用lockMap需要加上两层锁。
那么我们是不是可以直接使用Synchronized
来实现细粒度的锁机制
- public class LockExample {
- public static void syncFunc1(Long accountId) {
- String lock = new String(accountId + "").intern();
-
- synchronized (lock) {
-
- System.out.println(Thread.currentThread().getName() + "拿到锁了");
- // 模拟业务耗时
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
-
- System.out.println(Thread.currentThread().getName() + "释放锁了");
- }
- }
-
- public static void syncFunc2(Long accountId) {
- String lock = new String(accountId + "").intern();
-
- synchronized (lock) {
-
- System.out.println(Thread.currentThread().getName() + "拿到锁了");
- // 模拟业务耗时
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
-
- System.out.println(Thread.currentThread().getName() + "释放锁了");
- }
- }
-
- // 使用 Synchronized 来实现更加细粒度的锁
- public static void main(String[] args) {
- new Thread(()-> syncFunc1(123456L), "Thread-1").start();
- new Thread(()-> syncFunc2(123456L), "Thread-2").start();
- }
- }
-
- /**
- * 打印
- * Thread-1拿到锁了
- * Thread-1释放锁了
- * Thread-2拿到锁了
- * Thread-2释放锁了
- */

intern()
函数的功能;intern()
函数用于在运行时将字符串添加到堆空间中的字符串常量池中,如果字符串已经存在,返回字符串常量池中的引用。核心问题:我们需要找到一个多个进程之间所有线程可见的区域来定义这个互斥量。
一个优秀的分布式锁的实现方案应该满足如下几个特性:
setnx key value
来实现分布式锁;区别于普通set
指令的是只有当key不存在时才会设置成功,key存在时会返回设置失败代码实例:
- // 扣库存接口
- @RequestMapping("/minusInventory")
- public String minusInventory(Inventory inventory) {
- // 获取锁
- String lockKey = "lock-" + inventory.getInventoryId();
- int timeOut = 100;
- Boolean flag = stringRedisTemplate.opsForValue()
- .setIfAbsent(lockKey, "竹子-熊猫",timeOut,TimeUnit.SECONDS);
- // 加上过期时间,可以保证死锁也会在一定时间内释放锁
- stringRedisTemplate.expire(lockKey,timeOut,TimeUnit.SECONDS);
-
- if(!flag){
- // 非阻塞式实现
- return "服务器繁忙...请稍后重试!!!";
- }
-
- // ----只有获取锁成功才能执行下述的减库存业务----
- try{
- // 查询库存信息
- Inventory inventoryResult =
- inventoryService.selectByPrimaryKey(inventory.getInventoryId());
-
- if (inventoryResult.getShopCount() <= 0) {
- return "库存不足,请联系卖家....";
- }
-
- // 扣减库存
- inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
- int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
- } catch (Exception e) { // 确保业务出现异常也可以释放锁,避免死锁
- // 释放锁
- stringRedisTemplate.delete(lockKey);
- }
-
- if (n > 0)
- return "端口-" + port + ",库存扣减成功!!!";
- return "端口-" + port + ",库存扣减失败!!!";
- }

过期时间的合理性分析:
因为对于不同的业务,我们设置的过期时间的长短都会不一样,太长了不合适,太短了也不合适;
所以我们想到的解决方案是设置一条子线程,给当前锁资源续命。具体实现是,子线程间隔2-3s去查询一次key是否过期,如果还没有过期则代表业务线程还在执行业务,那么则为该key的过期时间加上5s。
但是为了避免主线程意外死亡后,子线程会一直为其续命,造成“长生锁”的现象,所以将子线程变为主(业务)线程的守护线程,这样子线程就会跟着主线程一起死亡。
- // 续命子线程
- public class GuardThread extends Thread {
- private static boolean flag = true;
-
- public GuardThread(String lockKey,
- int timeOut, StringRedisTemplate stringRedisTemplate){
- ……
- }
-
- @Override
- public void run() {
- // 开启循环续命
- while (flag){
- try {
- // 先休眠一半的时间
- Thread.sleep(timeOut / 2 * 1000);
- }catch (Exception e){
- e.printStackTrace();
- }
- // 时间过了一半之后再去续命
- // 先查看key是否过期
- Long expire = stringRedisTemplate.getExpire(
- lockKey, TimeUnit.SECONDS);
- // 如果过期了,代表主线程释放了锁
- if (expire <= 0){
- // 停止循环
- flag = false;
- }
- // 如果还未过期
- // 再为则续命一半的时间
- stringRedisTemplate.expire(lockKey,expire
- + timeOut/2,TimeUnit.SECONDS);
- }
- }
- }
-
-
- // 创建子线程为锁续命
- GuardThread guardThread = new GuardThread(lockKey,timeOut,stringRedisTemplate);
- // 设置为当前 业务线程 的守护线程
- guardThread.setDaemon(true);
- guardThread.start();

为了在开发过程保证Redis的高可用,会采用主从复制架构做读写分离,从而提升Redis的吞吐量以及可用性。但是如果一条线程在redis主节点上获取锁成功之后,主节点还没有来得及复制给从节点就宕机了,此时另一条线程访问redis就会在从节点上面访问,同时也获取锁成功,这时候临界资源的访问就会出现安全性问题了。
解决办法:
Zookeeper数据区别于redis的数据,数据是实时同步的,主节点写入后需要一半以上的节点都写入才会返回成功。所以如果像电商、教育等类型的项目追求高性能,可以放弃一定的稳定性,推荐使用redis实现;例如像金融、银行、政府等类型的项目,追求高稳定性,可以牺牲一部分性能,推荐使用Zookeeper实现。
上面加锁确实解决了并发情况下线程安全的问题,但是我们面对100w个用户同时去抢购1000个商品的场景该如何解决呢?
近 10 年间,甚至连传统企业都开始大面积数字化时,我们发现开发内部工具的过程中,大量的页面、场景、组件等在不断重复,这种重复造轮子的工作,浪费工程师的大量时间。
针对这类问题,低代码把某些重复出现的场景、流程,具象化成一个个组件、api、数据库接口,避免了重复造轮子。极大的提高了程序员的生产效率。
介绍一款程序员都应该知道的软件 JNPF 快速开发平台,很多人都尝试用过它,它是功能的集大成者,任何信息化系统都可以基于它开发出来。
这是一个基于 Java Boot/.Net Core 构建的简单、跨平台快速开发框架。前后端封装了上千个常用类,方便扩展;集成了代码生成器,支持前后端业务代码生成,实现快速开发,提升工作效率;框架集成了表单、报表、图表、大屏等各种常用的 Demo 方便直接使用;后端框架支持 Vue2、Vue3。
应用体验: https://www.jnpfsoft.com/?csdn,如果你有闲暇时间,可以做个知识拓展。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。