java的锁synchronied和本地锁(reentrantLock)与分布式锁(redisson)的底层实现

本文详细分析了ReentrantLock的公平锁和非公平锁的区别,以及它们如何通过等待队列实现公平性。同时介绍了Redisson中锁的获取、释放逻辑,包括公平锁、读写锁的实现、锁过期唤醒机制和防止A锁B解的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Synchronized锁

synchronized锁有什么特性,怎么解决并发问题

解决可见性问题

屏障-monitorenter-读屏障 保证读取到的数据都是最新的,monitorExit-写屏障 保证数据都写入到主存中。

解决原子性问题

锁的特性,只有获取锁的线程,才能执行代码。保证操作的原子性。

synchronized是怎么去优化性能的

锁升级的过程 - 更具并发的场景,采用不同的加锁方式。(重量级锁,需要使用操作系统的mutex,涉及用户态和内核态的切换。 所以在锁竞争少的情况下,使用其它的加锁方式)

偏向 -》轻量:有第二线程尝试获取锁,可自旋等待

轻量-》重量:第二个线程自旋一段时间依然没有获取到锁。或者有更多的线程来抢锁,那就升级为重量级锁。(核心原则:短时间自旋的效率优于阻塞线程)

锁监视器是什么

升级为重量级锁后,对象头的mark world 会有指针指向锁监视器。

作用:记录锁的拥有者,记录锁的加锁次数,记录等待池(调用wait方式,出于等待状态的线程)和阻塞池(竞争锁失败的线程)

synchronized怎么实现的锁的自旋,阻塞,可重入

自旋:while循环,短时间性能好。长时间耗cpu。

阻塞:使用操作系统的mutex指令

可重入:基于锁监控器中的持有锁线程和加锁次数

ReentrantLock

实现原理

通线程变量记录持有锁的线程。

再通过使用voletile修饰的int变量state表示加锁状态,已经可重入锁的次数

再通过AQS的先进先出队列来实现阻塞线程的存储,实现公平锁的特性

阻塞实现:通过LockSupport.park, 将线程置为waiting状态

锁释放:当持有线程的锁释放锁时,会通过LockSupport.unpark

参考美团文章:

从ReentrantLock的实现看AQS的原理及应用 - 美团技术团队

公平锁和非公平锁相同之处

都会使用到等待队列,并且队列中,先来的线程会优先获取到锁资源。所以其实就算是非公平锁,等待队列中的线程也还是公平的。

排队队列,使用的是双向链表的结构。

公平锁和非公平锁不同之处

hasQueuedPredecessors 实现的效果是。如果等待队列中有值,那么尝试当前线程,不能直接去获取锁资源。需要加入等待队列中排队。

结论:reentrantLock的非公平锁,对于等待中的线程也是有个公平的机制的,只是没有控制新来的线程(无锁状态下,可以直接尝试获取锁)。而公平锁解决了这个问题(无锁状态下,也不能直接获取锁,需要加入等待队中排队;除非等待队列为空,才能直接尝试获取)。

补充下:

ReentrantLock获取锁的方法有好几个,每个都有一点小差别,但是整体实现思路差不多。

lock方法:会一直尝试获取锁资源。

trylock方法不加时间:只会尝试一次获取锁资源。拿不到,就返回失败。没有使用到队列。

trylock加时间:一段时间内获取不到锁资源会失败。如果截止时间到现在的时间是大于1微秒的,则线程休眠(基本上就是休眠了),否则继续尝试获取。

读锁和写锁的底层实现

首先明确下读写锁的实现效果:

读读不互斥;读写互斥;

写写互斥;写读互斥;

总结:读写场景互斥。但是读场景下的读操作不互斥,写场景下的写操作之间互斥。

实现原理:

读锁使用的共享锁;写锁使用的是排他锁。

正常情况下,共享锁和排他锁不能同时存在,这样就可以实现读写场景互斥。(但是如果是同一线程,同时持有读写锁是可以的)

共享锁,允许多个线程同时持有。可以实现,读场景下的读线程之间不互斥的效果。

排他锁,不允许多个线程同时持有。可以实现,写场景下的写线程之间互斥的效果。

下面可以看看代码是如何实现的。

如果不能获取到锁资源会怎么处理?

和上面一样。读写锁也支持公平和非公平两种模式。

redisson

1. redisson如何实现尝试获取锁的逻辑

  • 如何实现在一段的时间内不断的尝试获取锁
    • 其实就是搞了个while循环,不断的去尝试获取锁资源。但是因为latch的存在会在给定的时间内处于休眠状态。
    • 这个事件,监听的是解锁动作,如果解锁动作发生。会调用latch.release方法,这样while循环又可以重新启动,去尝试获取锁资源了。(相比单纯的轮训,避免了对cpu资源的浪费。通过信号通知,避免了没必要的轮训)
  • 尝试获取锁的过程是怎样的?
    • 使用了redis脚本执行的方式。因为存在根据查询结果,来决定执行什么变更动作。所以一定要保证动作串行执行。如果key不存,则新增key和param(线程id+redisClientid)记录,value为数值型,value=1。  如果,key + param存在,则表名key已被默认线程持有,并且这个线程就是当前线程。如果,key + param不存在,则表明key已被默认线程持有,并且这个线程不是当前线程。

2. redisson释放锁的逻辑如何实现

因为加锁时,会设置过期时间。所以就算不主动解锁。key过期了就相当于解锁了。

redisson的解锁过程如下图。先判断线程是否持有该锁。如果有,则value值减1。然后判断value是否大于0,如果大于0,则给key设置一个默认的过期时间30秒;如果等于0,则可以删除key和发布一个key删除事件。

3. redisson释放锁时,如何唤起其它线程取争抢锁

很简单,使用发布订阅的机制。

释放锁时,发布锁释放消息。由于,争抢锁的线程在之前就订阅了这个消息。所以在接收到锁释放的消息后,就会立即再次尝试获取锁资源。

4. redisson如何解决A线程加锁,但是B线程去释放锁的问题

redi支持key+param的方式进行匹配。这里的param有点像标签。当线程A获取到redis的锁资源后,会将param设置为线程id+redisson连接管理器id。然后解锁的时候,也是需要带上parma匹配的。匹配不上是解锁不了的。
redis.call('hexists', KEYS[1], ARGV[2])

5. redisson如何实现公平锁?

这里使用了和ReentrantLock一样的思路,使用了等待队列。

大概的逻辑:

1. 如果当前不是加锁状态,并且等待队列是空。则当前线程可以直接获取锁。

2. 如果当前不是加锁状态,但是等待队列不为空。则当前线程需要加入等待队列中,排队获取锁。

3. 等待中的线程如何被唤醒。依然是通过消息通知的方式。在公平锁时,需要从队列头取出线程id,只有当前线程和取出的线程id匹配才能执行,否则继续休眠。

可以看看这段代码:org.redisson.RedissonFairLock#tryLockInnerAsync

6. redisson如何实现读写锁?

读锁加锁过程

读锁释放过程

写锁的获取过程

写锁的释放过程

7. redisson的锁如果是自己过期的,是怎么唤醒其他等待线程的?

redis是有key过期事件这样的特性的。当锁的过期时间到期,Redis 会触发一个事件通知,然后 Redisson 的订阅者会接收到这个通知。并唤醒订阅的线程。

总结

读锁获取过程

读锁释放过程

写锁获取场景

写锁释放

公平锁的实现方式

8. redisson是如何实现分布式的key续期的

使用一个看门狗机制,其实就是一个延时任务,去check客户端持有的key,如果存在就将key的过期时间设置为30秒,然后再生成一个10秒后的延时任务,任务这块会使用线程池处理(默认值16)。这样就不用担心因为客户端宕机导致锁一直释放不了。

9. 分布式锁怎么实现?

首先什么是锁

我们这块说的说指的是独占锁,具有互斥性,一个线程获取到锁,另外的线程就获取不到锁。

加锁的过程,保证原子性

方法1:使用setnx

方法2:使用lua脚本

解锁过程

为保证线程释放错锁,解锁前需要判断线程是否持有该锁,依赖redis的param参数实现(线程id+redisson连接管理器id)

锁阻塞如何实现

使用AQS阻塞队列,获取不到锁的线程进入等待列中排队。当持有锁的线程释放锁时,再通过消息通知的方式环形队列头部的线程。

怎么实现可重入锁

可重入锁,说的是持有锁的线程,多次尝试获取同一把锁。可以在尝试获取锁时,check持有锁的线程id,如果是同一个线程,加锁次数加1。 在释放锁时,也是释放一次,加锁次数减1。 减到0,表示持有锁的线程,释放了锁资源

怎么解决服务宕机后,锁无法释放的问题

获取锁时,默认都会加一个过期时间。然后通过延时任务的方式进行续期。宕机后延时任务都不存在了,肯定是不会续期了。但是如果是加锁线程中断后,没有释放锁,会导致,分布式锁,一直被无效续期,所以解锁动作一定要做finally代码块中执行。

10. 看门狗线程是个什么样的线程

可以理解成一个守护线程(使用延时任务实现),定时给key续过期时间。当锁释放后,延时任务不在执行。redission中执行延时任务的是一个线程数为16的线程池。

12. redis主从架构带来的锁丢失问题怎么解决?

红锁,会尝试所有的锁,但只要获取到多数锁就算获取成功。但是按红锁多少锁的原理,发生主从替换时,依然是会有问题的。但是发生的概率会很小(正常情况下,会拿到所有的锁,只拿到半数+1的锁的情况还是非常小的,解决了绝大数场景下锁丢失的问题)。可以使用联锁的方式解决。 你可能会担心加锁的时效和主节点宕机的问题。网络波动和主节点宕机,会造成很多的业务问题。个人感觉还好。

其它问题:

1. 当然红锁还是会有不同主节点的时钟问题

2. 系统阻塞导致锁未被续期,直接被释放

但是, redisson官方其实是已经废弃了红锁机制的。

原因:多数锁依然有锁丢失问题,第二无法控制不同的锁在不同的节点,也就是说有可能出现在单一节点。

Redisson 分布式锁源码 09:RedLock 红锁的故事_Java_程序员小航_InfoQ写作社区

其实可以不用把这个问题看的过重。 首先有并发也不一定有并发问题,且在叠加主从替换的场景概率是很低的。可以考验,如果有并发问题发生,怎么去发现,然后再采用一些修正的方法来保证最终一致性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值