一、分布式是什么?
分布式是指一个系统由多个独立的计算机节点组成,这些节点通过网络通信、协调工作,对外像一台计算机一样提供服务。
核心特点
- 多个节点:物理或虚拟机上运行独立进程。
- 消息通信:节点间通过 RPC、HTTP、消息队列等交换信息。
- 无共享架构:通常不共享内存或时钟,各自有本地资源。
- 部分失败:某个节点挂了,系统整体还能运行(但可能降级)。
简单例子
一个网站的后端由 10 台服务器组成,用户请求被负载均衡分发到其中一台;用户感觉不到是多台机器。
二、分布式锁的用途
分布式锁用于在分布式环境中控制**多个进程(或节点)**对共享资源的互斥访问。
典型用途
- 防止重复执行
比如定时任务调度,只希望集群中一个节点执行,避免重复扣款、重复发邮件。 - 保护共享资源
例如更新数据库中的同一行记录、操作共享文件、修改 Redis 中的计数值。 - 解决竞态条件
多个服务同时修改同一订单状态,用锁保证顺序。 - 实现领导者选举
利用锁(如 etcd、ZooKeeper 的临时顺序节点)让只有一个节点成为主节点。
与单机锁(如 Java
synchronized)的区别:单机锁只能锁住同一个 JVM 内的线程,分布式锁能锁住不同机器上的进程。
三、可重入性是什么?为什么要使用同一把锁?
1. 可重入性定义
可重入锁 (Reentrant Lock)指的是:同一个线程(或同一个客户端)在已经持有某把锁的情况下,可以再次成功获取该锁,而不会被阻塞。
锁内部会记录持有锁的线程(或客户端标识)和一个重入次数:
- 第一次获取:次数 = 1
- 再次获取(重入):次数 +1
- 释放一次:次数 -1,直到次数为 0 时真正释放锁。
2. 为什么要用同一把锁,而不是不同的锁或者干脆只上一次锁?
关键问题:为什么可重入要求的是"同一把锁"?
假设有一个场景:某个方法 doWork() 需要加锁,而它内部又调用了另一个也需要同一资源锁的方法 subWork()。
-
如果使用同一把锁(可重入) :
doWork获得锁 L,调用subWork时发现锁 L 已经被当前线程持有,直接放行(重入计数+1)。
结果:正常工作,避免死锁。 -
如果使用不同的锁 :
doWork获得锁 L1,subWork需要锁 L2。此时如果 L1 和 L2 保护的是同一资源(例如同一个银行账户),用两个不同锁就无法互斥------其他线程可能通过 L2 同时修改资源,破坏一致性。
-
为什么一个线程要"两次获取同一把锁"?只外层上一次锁,不就行了吗? :
因为内层函数可能被单独调用,它必须自己保证线程安全。如果为了迁就外层调用而取消内层的锁,就会破坏内层函数的封装性和安全性。
核心原因:
- 可重入要求"同一把锁"是为了在同一个线程内避免死锁(当嵌套调用需要同一资源时)。
- 如果换成不同的锁,要么失去互斥性(不同的锁保护同一资源),要么失去可重入语义(新锁对象与旧锁无关)。
简单记忆:可重入 = 同一个锁对象 + 持有者身份 + 计数。如果不是同一个锁,就无法判断"当前线程是否已经拥有对该资源的访问权",也就无法实现安全的重入。
四、总结
| 问题 | 回答 |
|---|---|
| 为什么不只上一次锁? | 因为内层函数(如 transferOut)可能被其他不持有锁的上下文调用,它必须自己加锁才能保证安全。 |
| 为什么需要同一把锁两次? | 外层函数调用了内层函数,两者需要保护同一资源(如账户余额),必须用同一把锁。可重入锁允许同一线程重复获取,避免死锁。 |
| 如果不用可重入锁怎么办? | 要么忍受死锁,要么破坏封装(传递锁状态参数),要么代码重复(写两个版本的方法)。 |
一句话总结 :可重入锁让你在保持方法独立线程安全 的同时,又能安全地嵌套调用这些方法。