什么是锁,为什么使用锁

用俗语来说,锁意味着一种保护,对资源的一种保护,在程序员眼中,这个资源可以是一个变量,一个代码片段,一条记录,一张数据库表等等。

就跟小孩需要保护一样,不保护的话小孩会收到伤害,同样的使用锁的原因是资源不保护的话,可能会受到污染,在并发情况下,多个人对同一资源进行操作,有可能导致资源不符合预期的修改。

常见的锁的种类

锁的种类细分的话,非常多,主要原因是从不同角度看,对锁的定义不一样,我这里总结了一下,画一个思维脑图,大家了解一下。

我个人认为锁都可以归为一下四大类,其它的叫法不同只是因为其实现方式或者应用场景而得名,但本质上上还是下面的这四大类中一种。

其它各种类的锁总结如下,这些锁只是为了高性能,为了各种应用场景在代码实现上做了很多工作,因此而得名,关于他们的资料很多

更多锁的详细解释参考我github的名词描述,这里不在赘述,地址如下:

https://github.com/sunpengwei1992/java_common/tree/master/src/lock

Go中的锁使用和实现分析

Go的代码库中为开发人员提供了一下两种锁:

  1. 互斥锁 sync.Mutex
  2. 读写锁 sync.RWMutex

第一个互斥锁指的是在Go编程中,同一资源的锁定对各个协程是相互排斥的,当其中一个协程获取到该锁时,其它协程只能等待,直到这个获取锁的协程释放锁之后,其它的协程才能获取。

第二个读写锁依赖于互斥锁的实现,这个指的是当多个协程对某一个资源都是只读操作,那么多个协程可以获取该资源的读锁,并且互相不影响,但当有协程要修改该资源时就必须获取写锁,如果获取写锁时,已经有其它协程获取了读写或者写锁,那么此次获取失败,也就是说读写互斥,读读共享,写写互斥。

Go中关于锁的接口定义如下:,该接口的实现就是上面的两个锁种类,篇幅有限,这篇文章主要是分析一下互斥锁的使用和实现,因为RWMutex也是基于Mutex的,大家可以参考文章自行学习一下。

type Locker interface {
Lock()
Unlock()
}
type Mutex struct {
state int32 //初始值默认为0
sema uint32 //初始值默认为0
}

Mutex使用也非常的简单,,声明一个Mutex变量就可以直接调用Lock和Unlock方法了,如下代码实例,但使用的过程中有一些注意点,如下:

  1. 同一个协程不能连续多次调用Lock,否则发生死锁
  2. 锁资源时尽量缩小资源的范围,以免引起其它协程超长时间等待
  3. mutex传递给外部的时候需要传指针,不然就是实例的拷贝,会引起锁失败
  4. 善用defer确保在函数内释放了锁
  5. 使用-race在运行时检测数据竞争问题,go test -race ....,go build -race ....
  6. 善用静态工具检查锁的使用问题
  7. 使用go-deadlock检测死锁,和指定锁超时的等待问题(自己百度工具用法)
  8. 能用channel的场景别使用成了lock
var lock sync.Mutex

func MutexStudy(){
//获取锁
lock.Lock()
//业务逻辑操作
time.Sleep(1 * time.Second)
//释放锁
defer lock.Unlock()
}

我们了解了Mutext的使用和注意事项,那么具体原理是怎么实现的呢?运用到了那些技术,下面一起分析一下Mutex的实现原理。

Mutex实现中有两种模式,1:正常模式,2:饥饿模式,前者指的是当一个协程获取到锁时,后面的协程会排队(FIFO),释放锁时会唤醒最早排队的协程,这个协程会和正在CPU上运行的协程竞争锁,但是大概率会失败,为什么呢?因为你是刚被唤醒的,还没有获得CPU的使用权,而CPU正在执行的协程肯定比你有优势,如果这个被唤醒的协程竞争失败,并且超过了1ms,那么就会退回到后者(饥饿模式),这种模式下,该协程在下次获取锁时直接得到,不存在竞争关系,本质是为了防止协程等待锁的时间太长。

两种模式都了解了,我们再来分析一下几个核心常量,代码如下:

const (
mutexLocked = 1 << iota //1, 0001 最后一位表示当前锁的状态,0未锁,1已锁
mutexWoken //2, 0010,倒数第二位表示当前锁是否会被唤醒,0唤醒,1未唤醒
mutexStarving //4, 0100 倒数第三位表示当前对象是否为饥饿模式,0正常,1饥饿
mutexWaiterShift = iota //3 从倒数第四位往前的bit表示排队的gorouting数量
starvationThresholdNs = 1e6 // 饥饿的阈值:1ms

//Mutex中的变量,这里主要是将常量映射到state上面
state //0代表未获取到锁,1代表得到锁,2-2^31表示gorouting排队的数量的
sema //非负数的信号量,阻塞协程的依据

这几个变量你要是都弄白了,那么代码看起来就相对好理解一些了,整个Lock的源码较长,我将注释写入代码中,方便大家理解,整个锁的过程其实分为三部分,建议大家参考源码和我的注释一块学习。

  1. 直接获取锁,返回
  2. 自旋和唤醒
  3. 判断各种状态,特殊情况处理

第一部分代码如下,较为简单,获取锁成功之后直接返回

//对state进行cas修改操作,修改成功相当于获取锁,修改之后state=1
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}

第二部分自旋的代码如下

//开始等待时间
var waitStartTime int64
//这几个变量含义依次是:是否饥饿,是否唤醒,自旋次数,锁的当前状态
starving := false;awoke := false;iter := 0;old := m.state
//进入死循环,直到获得锁成功(获得锁成功就是有别的协程释放锁了)
for {
//这个if的核心逻辑是判断:已经获得锁了并且不是饥饿模式 && 可以自旋,与cpu核数有关
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
//这个是判断:没有被唤醒 && 有排队等待的协程 && 尝试设置通知被唤醒
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
//说明上个协程此时已经unlock了,唤醒当前协程
awoke = true
}
//自旋一段时间
runtime_doSpin()
//自选次数加1
iter++
old = m.state
continue
}
}

第三部分代码,判断各种状态,特殊情况处理

new := old
//1:原协程已经unlock了,对new的修改为已锁
if old&mutexStarving == 0 {
new |= mutexLocked
}
//2:这里是执行完自旋或者没执行自旋(原协程没有unlock)
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift //排队
}
//3:如果是饥饿模式,并且已锁的状态
if starving && old&mutexLocked != 0 {
new |= mutexStarving //设置new为饥饿状态
}
//4:上面的awoke被设置为true
if awoke {
//当前协程被唤醒了,肯定不为0
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
//既然当前协程被唤醒了,重置唤醒标志为0
new &^= mutexWoken
}
//修改state的值为new,但这里new的值会有四种情况,
//就是上面4个if情况对new做的修改,这一步获取锁成功
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&(mutexLocked|mutexStarving) == 0 {
//这里代表的是正常模式获取锁成功
break
}
//下面的代码是判断是否从饥饿模式恢复正常模式
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
waitStartTime = runtime_nanotime()
}
//进入阻塞状态
runtime_SemacquireMutex(&m.sema, queueLifo)
//设置是否为饥饿模式,等待的时间大于1ms就是饥饿模式
starving=starving||runtime_nanotime()-waitStartTime> starvationThresholdNs
old = m.state
//如果当前锁是饥饿模式,但这个gorouting被唤醒
if old&mutexStarving != 0 {
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
//减去当前锁的排队
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
//退出饥饿模式
delta -= mutexStarving
}
//修改状态,终止
atomic.AddInt32(&m.state, delta)
break
}
}
//设置被唤醒
awoke = true
iter = 0
} else {
old = m.state
}

Lock的源码我们弄明白了,那么Unlock呢,大家看代码的时候最好Lock和Unlock结合一起来看,因为他们是对同一变量state在操作

func (m *Mutex) Unlock() {
//释放锁
new := atomic.AddInt32(&m.state, -mutexLocked)
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
//判断当前锁是否饥饿模式,==0代表不是
if new&mutexStarving == 0 {
old := new
for {
//如果没有未排队的协程 或者 有已经被唤醒,得到锁或饥饿的协程,则直接返回
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
//唤醒其它协程
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false)
return
}
old = m.state
}
} else {
//释放信号量
runtime_Semrelease(&m.sema, true)
}
}

到这里整个Mutex的源码分析完成,可以看到Metux的源码并不是很复杂,只是各种位运算让开发人员难以直接观察到结果值,另外阅读源码前一定要先明白各个变量和常量的含义,不然读起来非常费劲。

![](https://img2018.cnblogs.com/blog/706455/202001/706455-20200113092119426-247247567.jpg)

Go中锁的那些姿势,估计你不知道的更多相关文章

  1. 解锁redis锁的正确姿势

    解锁redis锁的正确姿势 redis是php的好朋友,在php写业务过程中,有时候会使用到锁的概念,同时只能有一个人可以操作某个行为.这个时候我们就要用到锁.锁的方式有好几种,php不能在内存中用锁 ...

  2. 【分布式缓存系列】集群环境下Redis分布式锁的正确姿势

    一.前言 在上一篇文章中,已经介绍了基于Redis实现分布式锁的正确姿势,但是上篇文章存在一定的缺陷——它加锁只作用在一个Redis节点上,如果通过sentinel保证高可用,如果master节点由于 ...

  3. 【分布式缓存系列】Redis实现分布式锁的正确姿势

    一.前言 在我们日常工作中,除了Spring和Mybatis外,用到最多无外乎分布式缓存框架——Redis.但是很多工作很多年的朋友对Redis还处于一个最基础的使用和认识.所以我就像把自己对分布式缓 ...

  4. Redis全方位详解--数据类型使用场景和redis分布式锁的正确姿势

    一.Redis数据类型 1.string string是Redis的最基本数据类型,一个key对应一个value,每个value最大可存储512M.string一半用来存图片或者序列化的数据. 2.h ...

  5. 从DeepNet到HRNet,这有一份深度学习“人体姿势估计”全指南

    从DeepNet到HRNet,这有一份深度学习"人体姿势估计"全指南 几十年来,人体姿态估计(Human Pose estimation)在计算机视觉界备受关注.它是理解图像和视频 ...

  6. Sql Server 中锁的概念

    锁的概述 一. 为什么要引入锁 多个用户同时对数据库的并发操作时会带来以下数据不一致的问题: 丢失更新A,B两个用户读同一数据并进行修改,其中一个用户的修改结果破坏了另一个修改的结果,比如订票系统 脏 ...

  7. InnoDB中锁的查看

    Ⅰ. show engine innodb status\G 1.1 实力分析一波 锁介绍的那篇中已经提到了这个命令,现在我们开一个参数,更细致的分析一下这个命令 (root@localhost) [ ...

  8. SQL Server中锁与事务隔离级别

    SQL Server中的锁分为两类: 共享锁 排它锁 锁的兼容性:事务间锁的相互影响称为锁的兼容性. 锁模式 是否可以持有排它锁 是否可以持有共享锁 已持有排它锁 否 否 已持有共享锁 否 是 SQL ...

  9. InnoDB中锁的模式,锁的查看,算法

    InnoDB中锁的模式   Ⅰ.总览 S行级共享锁lock in share mode X行级排它锁增删改 IS意向共享锁 IX意向排他锁 AI自增锁 Ⅱ.锁之间的兼容性 兼 X IX S IS X ...

随机推荐

  1. 2018-8-10-win10-uwp-自定义控件初始化

    title author date CreateTime categories win10 uwp 自定义控件初始化 lindexi 2018-08-10 19:16:50 +0800 2018-2- ...

  2. laravel5.*安装使用Redis以及解决Class 'Predis\Client' not found和Fatal error: Non-static method Redis::set() cannot be called statically错误

    https://phpartisan.cn/news/35.html laravel中我们可以很简单的使用Redis,如何在服务器安装Redis以及原创访问你们可以访问Ubuntu 设置Redis密码 ...

  3. 七个用于数据科学(data science)的命令行工具

    七个用于数据科学(data science)的命令行工具 数据科学是OSEMN(和 awesome 相同发音),它包括获取(Obtaining).整理(Scrubbing).探索(Exploring) ...

  4. POJ 2251宽搜、

    因为这个题做了两次犯了两次不同的错误. 第一次用的dfs死活都超时 第二次把定义队列定义在了全局变量的位置,导致连WA了几次.最后找到原因的我真的想一巴掌拍死自己 #include<cstdio ...

  5. 5款顶尖Windows文件传输工具

    5款顶尖Windows文件传输工具 英文原文: Drasko 日常工作中,公司里的系统管理员或其他岗位的员工都需要传递大量各种类型的文件和文档.其中一些可以通过 email 收发.但由于 email ...

  6. 机器学习——集成学习之Bagging

    整理自: https://blog.csdn.net/woaidapaopao/article/details/77806273?locationnum=9&fps=1 随机森林 1.随机森林 ...

  7. 51nod 挑剔的美食家

    挑剔的美食家    基准时间限制:1 秒 空间限制:131072 KB 分值: 5 与很多奶牛一样,Farmer John那群养尊处优的奶牛们对食物越来越挑剔,随便拿堆草就能打发她们午饭的日子自然是一 ...

  8. 递归&时间模块&os模块

    递归 递归调用 一个函数,调用了自身,称为递归调用 递归函数:一个会调用自身的函数称为递归函数 凡是循环能干的事,递归都能干 方式: 写出临界条件 找这一次和上一次的关系 假设当前函数已经能用,调用自 ...

  9. vue-learning:6-template-v-bind

    绑定元素特性的指令v-bind 回顾下,从HTML元素的结构看,在VUE框架中,内容由插值{{ }}和v-html绑定:v-if和v-show可以控制元素的可见性:v-for可以用于批量生成列表元素. ...

  10. CSU 2323 疯狂的企鹅II (中位数的性质)

    Description 继在鹅厂工作的DJ训练完鹅厂的企鹅们之后,DJ发明了一个新游戏.该游戏在nxn的棋盘上进行,其中恰好有n个企鹅,企鹅向四个方向之一移动一格算作一步.DJ希望用最少的总步数把这些 ...