锁总结
2020-09-23 11:09:01 0 举报
AI智能生成
互斥锁,共享锁,乐观锁,悲观锁特性总结
作者其他创作
大纲/内容
互斥锁
互斥锁枷锁失败后,线程会释放cpu,给其它线程
互斥锁是一种独占锁,当线程A加锁成功后,此时互斥锁已经被线程A独占,只要线程A没有释放手中的锁,线程B加锁就会失败,
于是就会将cpu转给其它的线程,既然已经释放了cpu,那么B加锁的代码就会被阻塞。
于是就会将cpu转给其它的线程,既然已经释放了cpu,那么B加锁的代码就会被阻塞。
互斥锁加锁失败被阻塞现象是系统内核来完成,加锁失败,内核会
将线程置为“睡眠”状态,锁释放后,内核会在合适时机唤醒线程,
当获取到锁后,就可以成功执行。
将线程置为“睡眠”状态,锁释放后,内核会在合适时机唤醒线程,
当获取到锁后,就可以成功执行。
互斥锁加锁失败后,会从用户态陷入到内核态,让内核帮忙切换线程,
虽然简化了用锁的难度,但是增加了用锁的开销。
虽然简化了用锁的难度,但是增加了用锁的开销。
两次线程切换上下文开销
线程加锁失败时,内核会将线程从运行切换到睡眠状态,
然后将cpu让给其它线程
然后将cpu让给其它线程
锁被释放时,之前睡眠的线程切换到就绪状态,
内核会在合适的时机,将cpu让给该线程使用
内核会在合适的时机,将cpu让给该线程使用
线程的上下文切换是什么?
当两个线程同属于一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存保持不动,
只需要切换线程的私有数据,寄存器等不共享的数据
只需要切换线程的私有数据,寄存器等不共享的数据
线程切换耗时大概在几十纳秒到几微秒之间,如果锁住的代码执行时间较短,那
可能上下文切换的时间都比锁住的代码的执行时间还要长。
可能上下文切换的时间都比锁住的代码的执行时间还要长。
自旋锁
自旋锁加锁失败后,线程会忙等待,直到它拿到锁
自旋锁通过cpu提供的CAS函数(compare and swap), 在用户态
完成加锁和解锁操作,不主动产生线程上下文切换, 所以相比互斥锁来说
会快一些,开销也小一些。
完成加锁和解锁操作,不主动产生线程上下文切换, 所以相比互斥锁来说
会快一些,开销也小一些。
加锁包含2个步骤:1.查看锁状态,如果锁是空闲的,则执行第二步
2.将锁设置为当前线程持有;
CAS将这2个步骤合并成一条硬件级指令,形成原子指令,这样保证2个步骤
不可分割,要么不执行,要么都执行完。
2.将锁设置为当前线程持有;
CAS将这2个步骤合并成一条硬件级指令,形成原子指令,这样保证2个步骤
不可分割,要么不执行,要么都执行完。
当使用自旋锁时候,发生多线程竞争情况下, 加锁失败的线程会忙等待,直到它拿到锁,
自旋锁是比较简单的一种锁,开销小,多核系统下一般不会主动产生线程切换,适合异步,协程等
在用户态请求的编程方式,但是如果被锁住的代码执行时间较长,自旋锁会
长时间占用cpu资源,所以自旋的时间和被锁住的代码执行时间是成正比的。
在用户态请求的编程方式,但是如果被锁住的代码执行时间较长,自旋锁会
长时间占用cpu资源,所以自旋的时间和被锁住的代码执行时间是成正比的。
自旋锁和互斥锁在加锁失败的时候应对策略不一样:当加锁失败时,互斥锁用线程切换的方式来应对,
而自旋锁采用忙等待的方式来应对
而自旋锁采用忙等待的方式来应对
互斥锁和自旋锁是最基本的处理方式,更高级的锁都会选择其中一个来实现,比如读写锁,既可以选择互斥锁来实现,也可以基于自旋锁来实现
读写锁
读写锁适用于能明确区分读操作和写操作的场景
工作原理:当写锁没有被线程持有时,多个线程能够并发的持有读锁,多个线程同时持有读锁也不回破坏共享资源的数据,
一旦写锁被线程持有,读线程的读锁操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。
所以写锁是独占锁,因为任意一个时刻只有一个线程持有写锁,类似互斥锁和自旋锁, 而读锁是共享锁,因为读锁可以被多个线程
同时持有。
一旦写锁被线程持有,读线程的读锁操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。
所以写锁是独占锁,因为任意一个时刻只有一个线程持有写锁,类似互斥锁和自旋锁, 而读锁是共享锁,因为读锁可以被多个线程
同时持有。
根据实现不同,读写锁可以分为读优先锁和写优先锁
读优先锁:期望的是读锁能被更多的线程持有,以便提高读线程的并发性,工作方式:
当读线程A持有了读锁,写线程B在获取写锁的时候,会被阻塞,并且阻塞过程中,后续来的
读线程C仍然可以成功获取读锁,直到读线程A和C释放读锁后,写线程B才可以成功获取读锁
当读线程A持有了读锁,写线程B在获取写锁的时候,会被阻塞,并且阻塞过程中,后续来的
读线程C仍然可以成功获取读锁,直到读线程A和C释放读锁后,写线程B才可以成功获取读锁
写优先锁:优先服务写线程,工作方式是:当读线程A先持有了读锁,写线程B在获取写锁时,会被阻塞,并且在阻塞过程中
后续来的读线程C获取读锁会失败,于是线程C被阻塞在获取读锁的阶段,这样只要读线程A释放读锁后,写线程B就可以成功获取到
写锁。
后续来的读线程C获取读锁会失败,于是线程C被阻塞在获取读锁的阶段,这样只要读线程A释放读锁后,写线程B就可以成功获取到
写锁。
读优先锁对于读线程并发性更高,但是也不是没问题,试着想下,如果一直有读线程获取到读锁,那么写线程将永远获取不到写锁,
就造成了写线程饥饿,写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,那么读线程也会被饿死。
就造成了写线程饥饿,写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,那么读线程也会被饿死。
公平读写锁
一种简单的实现方式为:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样
不会出现饥饿现象。
不会出现饥饿现象。
乐观锁
乐观锁做事比较乐观, 它假定冲突概率很低,它的工作方式为:先修改完共享资源, 再验证这段时间内有没有冲突,
如果没有其它线程在修改资源,那么操作完成,如果发现有其它线程已经修改资源,就放弃本次操作, 虽然重试成本很高,
但是冲突概率足够低的话,还是可以接受。
如果没有其它线程在修改资源,那么操作完成,如果发现有其它线程已经修改资源,就放弃本次操作, 虽然重试成本很高,
但是冲突概率足够低的话,还是可以接受。
乐观锁全程没有加锁,也叫无锁编程
使用场景:在线文档, svn, git
乐观锁去除了加锁操作,但是一旦发生冲突,重试成本很高,所以只有在冲突概率很低,且加锁成本很高的场景,才考虑使用乐观锁。
悲观锁
前面的互斥锁,自旋锁,读写锁都是悲观锁,悲观锁做事比较悲观,它认为多线程
同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先上锁,如果多线程同时修改共享资源的概率较低,可以使用乐观锁
同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先上锁,如果多线程同时修改共享资源的概率较低,可以使用乐观锁
多线程访问的时候,避免不了资源竞争而导致数据错乱的问题,通常为了解决这个问题,都会在访问共享资源之前加锁。
0 条评论
下一页