分布式锁必须的条件
- 独占性
- 高可用
- 防死锁
- 不乱抢
- 可重入
独占性
-
概念:锁的独占性又称为排他性,就是说一个资源被一个线程所占用,那么其他想访问本资源的线程都需要等待。
-
为什么需要:因为一个资源同时被多个线程操作就会出现重复写,导致数据成为脏数据不可用。例如AB线程同时需要将一个资源减一,正常情况总资源减二,但是若是A执行慢了,B在A执行的时候也要对资源操作,AB 拿到的总数都是一样的,各自去对总数操作减一,就会导致明明消耗了两个资源,记录却只有一个。
-
实现依据:
2.1 操作系统:互斥量(lock/unlock)、信号量(维护一个计数器,资源来了+1,请求来了-1,大于0说明有资源,小于0等待)
2.2 java:
2.2.1 synchronized:每个Java对象都关联着一个监视器(Monitor),当线程想要执行synchronized代码块或方法时,它必须先获得该对象的监视器。在字节码层面,synchronized通过monitorenter和monitorexit这两条指令来实现锁的获取和释放。monitorenter: 当线程进入synchronized代码块或方法时,会在其字节码中插入monitorenter指令。这会导致线程尝试获取对象的监视器锁。如果锁未被其他线程持有,则该线程获得锁并执行同步代码。如果锁已被持有,则该线程将阻塞,直到锁被释放。
monitorexit: 在同步代码块或方法结束(包括正常结束和因异常退出)时,会插入monitorexit指令来释放锁。确保锁总是能被正确释放,即使发生异常。
这个也就是相当于是字节码层次上添加互斥量
2.2.2 ReentrantLock : 基于AbstractQueuedSynchronizer(AQS)框架实现的。AQS使用了一个内部的FIFO等待队列来管理线程,当线程尝试获取锁失败时,会被封装成Node节点加入队列,然后阻塞等待。锁的释放会唤醒队列中的下一个等待线程。ReentrantLock支持公平和非公平模式,通过内部的公平性策略决定线程获取锁的顺序。
公平锁(FairSync):
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();// 获取当前线程
int c = getState(); // 得到当前锁的层数
if (c == 0) { // 可加锁
if (!hasQueuedPredecessors() && // 前面没有排队线程
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current); // 抢占成功
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 已经获得过了
int nextc = c + acquires;// 再加一层锁,不重新获取锁
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
非公平锁(NonfairSync):
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 少了 hasQueuedPredecessors
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2.3 redis、zookeeper、mysql: 这三个在实现独占性也是靠的互斥量的思路。
高可用
- 概念:不能因为某一个部分坏了,导致所有的项目服务都不能使用
- 为什么需要: 因为服务每多宕机一秒钟,损失就会指数级爆炸。
- 实现依据:主从备份,集群部署。
主从备份:master/slaver 的模式,可以保证即使一台机器坏了数据依然有备份还能用。
集群部署:多个主从备份+哨兵,可以将影响降到最小
防死锁
- 概念: 多个线程互相等待对方释放资源而永久地阻塞,导致它们都无法继续执行下去。简单来所就是AB两个线程工作需要同时拥有CD两个资源才能进行,但是现在A抢了C,B抢了D互相不释放自己的,导致两个线程一直处于等待状态。
- 为什么需要: 死锁多了,等待的线程就多了,内存就爆了,服务就挂了,你就被开了。
- 实现依据:从死锁产生的四个条件入手(互斥条件、请求与保持条件、不剥夺条件、循环等待条件),破坏形成条件即可破环死锁。
3.1 互斥条件:资源不共享,一个资源不能同时被多个线程操作。这个不好破坏哦,不能把一百块钱撕开一人当50去用啊。
3.2 请求与保持条件:线程工作是需要多个资源的,但是没得到所有的资源就不能干活,并且已经拿到手的资源也不会释放。就是你上班需要工资和电脑敲代码,但是公司一直不给你电脑,你不会说要把工资还回去吧,只能是一直等着发电脑干活。这个怎么破坏呢?一种是撤销,公司刚给你招进来就发现没钱买电脑了,直接撤回了一个offer,给你说拜拜~,一种是循环检测,公司人事天天巡逻,发现一个拿工资不干活的,就给你开了。还有一种就是给你补齐资源,让你把活干完,一般不会采取这个。
3.3 不剥夺条件: 进程已获得的资源在未使用完毕之前,不能被其他进程强行剥夺。就是说你得到的这个工作岗位,你不走之前别人也不能再获得。怎么破环呢?懂得都懂,直接给你开了就好了。
3.4 循环等待条件: 存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被链中下一个进程所请求。就是你有工资没电脑,别人有电脑没工资,两个人都犟得很,一个不给你电脑就不工作,一个人不给工资就不还电脑。两个人都在无限期的等待。
不乱抢
- 概念: 自己加的锁只能自己去释放。
- 为什么需要:大家都能解别人锁了,还加什么锁。
- 实现依据:每把锁都需要一个唯一标识,解锁之前先看看是不是自己锁。
可重入
- 概念:每个资源可能不止操作一次,那么我拿到这个锁以后,再次操作是不需要再重新获取一把新的锁。
- 为什么需要: 因为新建一把锁开销很大,并且若是操作次数多,还需要按顺序解锁很麻烦。
- 实现依据:给锁增加层数,用一次增加一层,那么解锁的时候就是释放一层,释放完了就删掉。