互斥锁和自旋锁:

互斥锁用保证共享资源在任一时间只有一个线程访问,当已经有一个线程加锁后,其他线程加锁就会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的:互斥锁加锁失败后,线程会释放CPU,给其他线程;自旋锁加锁失败后,线程会忙等待,直到它拿到锁。

互斥锁是一种独占锁,当线程A加锁成功时,互斥锁就被线程A独占了,只要线程A没有释放手中的锁,线程B加锁就会失败,线程B被阻塞,释放CPU。

互斥锁加锁失败线程阻塞是由操作系统内核实现的。当加锁失败时,内核会将线程置位睡眠状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程获取到锁后就可以继续执行下去。

所以互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮我们切换线程,存在两次线程上下文切换的成本:

1.线程加锁失败时,内核把线程从运行设置为睡眠,然后把CPU切换给其他进程。

2.当锁被释放时,之前睡眠的线程会变为就绪态,然后内核会在合适的时间把CPU切换给该线程运行。

线程的上下文切换:当两个线程是属于同一个进程,因为线程共享进程的虚拟内存,只需要切换线程的私有数据,寄存器等不共享的数据。

如果被锁住的代码执行时间很短,小于两次上下文切换的时间,就不应该使用互斥锁,而应该选用自旋锁。

自旋锁通过CPU提供的CAS函数(Compare And Swap),在用户态完成加锁和解锁操作,不会产生线程的上下文切换,所以速度开销小一些。CAS函数是一条原子指令,包含两个步骤:1.查看所得状态 2.如果锁是空闲的,就将锁设置为当前线程持有。

自旋锁是比较简单的一种锁,一直自旋,利用CPU,直到锁可用。需要注意,在单核CPU上,需要抢占式调度(不断通过时钟终端一个线程,运行其他线程)。如果采用非抢占式调度,会导致一个自旋的线程永远不会放弃CPU。

如果被锁住的代码执行时间过长,自旋的线程会长时间占用CPU资源,所以自旋的时间和被锁住代码的执行时间是成正比的关系。

自旋锁和互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,互斥锁用线程切换来应对,自旋锁则用忙等待来应对。

互斥锁和自旋锁时最底层的两种锁,更高级的锁都会选择其中一个来实现。比如读写锁既可以选择互斥锁实现,也可以基于自旋锁实现。

读写锁:

从字面意思也可以知道,它由读锁和写锁两部分构成,如果只读取共享资源用读锁加锁,如果要修改共享资源则用写锁加锁。

读写锁适用于能明确区分读操作和写操作的场景。

工作原理:

当写锁没有被线程持有时,多个线程能够并发的持有读锁,大大提高共享资源的访问效率。但是,一旦写锁被进程持有后,其他线程获取读锁和写锁都会被阻塞。

所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。知道了读写锁的工作原理后,我们可以发现,读写锁在读多写少的场景,能发挥出优势。

另外,根据实现的不同,读写锁可以分为读优先锁和写优先锁。

读优先锁:当读线程A先持有了读锁,线程B在获取写锁的时候,会被阻塞,并且在堵塞的过程中,后续来的读线程C仍然可以成功获取读锁,最后知道读线程A和C释放读锁后,写线程B才可以成功获取写锁。

写优先锁:当都线程A先持有了读锁,线程B在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程C在获取读锁时也会被阻塞,这样只要读线程A释放读锁后,写线程B就可以获取写锁。

读优先锁对于读线程的并发性更好,但是会导致写线程饥饿现象。写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被饿死。

不管是读优先锁还是写优先锁,对方都可能会出现饿死的问题,所以不偏袒任何一方,搞个公平读写锁:

用队列把获取锁的线程排队,不管是读线程还是写线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现饥饿的现象。

乐观锁与悲观锁:

互斥锁,自旋锁和读写锁都属于悲观锁。悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,很容易出现冲突,所以访问共享资源前,要先上锁。

相反的,如果多线程同时修改共享资源的概率比较低, 就可以采用乐观锁。乐观锁做事比较乐观,它鉴定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。放弃后重试的成本很高,但是如果冲突的概率足够低的话,还是可以接受的。可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,乐观锁全程并没有加锁,所以它也叫无锁编程。

乐观锁的应用场景:在线文档。在线文档是可以同时多人编辑的,如果使用了悲观锁,那么只要有一个用户在编辑文档,其他用户就无法打开相同的文档了。乐观锁允许多个用户打开同一个文档进行编辑,编辑完提交后才验证修改的内容是否有冲突。冲突的例子:用户A和B打开相同的文档进行编辑,但是用户B比用户A先提交改动,这一过程用户A是不知道的,当A提交修改完的内容时,那么A和B并行修改的地方就会发生冲突。服务器验证发生冲突的方案:用户的浏览器在下载文档时会记录服务端返回的文档版本号,当用户提交修改时,发给服务器的请求会带上该文档版本号,服务器将受到的版本号与当前版本号进行比较,如果一致则修改成功,否则修改失败。

实际上,我们常见的SVN和Git也使用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的低档,需要我们自己修改后,再重新提交。乐观锁虽然去除了加锁解锁的操作,但是一旦发生冲突,重试的成本非常高,所以只有在冲突(多线程同时修改共享资源)概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。


参考:

​https://blog.csdn.net/qq_34827674/article/details/108608566​

​https://blog.csdn.net/daaikuaichuan/article/details/82950711​