go语言sync包的mutex设计还是挺巧妙的,也看了网上其他介绍的文章,加锁的这个流程细节还是挺复杂,特别是里面用到了三个比特位来表示锁状态,我觉得主要明白其中心思想就好了。这里主要是利用goland编辑器的plantuml来画了一个时序图,熟悉一下plantuml的使用,感觉效果还行。
团队中多人开发,也可以用plantuml,如果其他人想修改流程,通过编辑plantuml代码可以很方便的修改图。而且画出来的效果看着还不错。
中心思想: 1.乐观锁,进来就尝试CAS修改锁状态进行抢锁 2.进入慢锁竞争等待流程 2.1 正常模式:唤醒等待队列时,会唤醒所有线程,谁先抢到锁就是谁的 2.2 饥饿模式:饥饿模式下排队,由释放锁的线程直接唤醒队列头部线程拿锁先到达的先获取锁,后到的进入队列尾部。
@startuml
'https://plantuml.com/sequence-diagram
'自动编号'
autonumber
client -> mutex: 请求加锁
activate mutex #FFBBBB
alt 通过CAS判断当前锁是否空闲\n并操作当前锁的状态
mutex -[#FF0000]-> client: 空闲则尝试加锁,返回成功
note right: 结束 注释
else 已加锁,进入慢加锁流程
loop 循环抢锁
alt 锁忙碌 AND 非饥饿模式 AND 系统可以自旋
alt 当前线程非唤醒状态 AND \n互斥锁没有已经苏醒的线程 AND \n有其他线程等待锁
mutex --> mutex: 如修改互斥锁状态为有唤醒者成功\n就把当前线程标记为唤醒
end
mutex --> mutex: 进入自旋 自旋记录+1 \n返回loop
end
alt 当前互斥锁不是饥饿模式
mutex-->mutex: 标记临时变量为加锁
end
alt 当前锁是忙碌或饥饿模式
mutex --> mutex: 标记临时变量队列等待数+1
end
alt 如果是当前线程已是饥饿状态 AND 当前锁忙碌
mutex-->mutex: 标记临时变量为饥饿模式
end
alt 如果当前线程是唤醒线程
mutex-->mutex: 标记临时变量为无唤醒线程
end
alt 原子操作,将临时变量保存到锁变量,如成功
alt 如果锁以前是空闲 AND 非饥饿
mutex --> client:抢锁成功
note right
这里是正常模式下
各个线程竞争抢锁,成功就返回
end note
end
mutex --> mutex: 阻塞等待信号量
mutex --> mutex: 其他释放锁,该线程被唤醒
mutex --> mutex: 检查自己的等待时间,判断是否进入饥饿状态
alt 当前锁是饥饿模式
mutex --> mutex: 等待线程数减1\n计算锁的饥饿状态
mutex --> mutex: 修改锁状态
mutex --> client: 抢锁成功
note right
这里是饥饿模式下
由释放锁的线程直接唤醒队列头部线程拿锁
其他等待线程继续等待
end note
end note
end
mutex --> mutex: 标记自己为苏醒状态
end
end
deactivate mutex
note left
中心思想
1.乐观锁,进来就尝试CAS修改锁状态进行抢锁
2.进入慢锁竞争等待流程
2.1 正常模式:唤醒等待队列时,会唤醒所有线程,谁先抢到锁就是谁的
2.2 饥饿模式:饥饿模式下排队,由释放锁的线程直接唤醒队列头部线程拿锁
先到达的先获取锁,后到的进入队列尾部
end note
@enduml
参考: