一文搞懂各种锁-互斥锁-自旋锁-可重入锁-读写锁-悲观锁-乐观锁-分布式锁

一 为什么会有锁机制

1  在多线程情况下共享操作同一个变量时,会导致数据不一致,出现并发安全问题,所以通过锁机制来保证数据的准确和唯一
2  通过锁将可能出现问题的代码用锁对象锁起来,被锁起来的代码就叫同步代码块,同一时间只能有一个线程来访问这个同步代码块

二 什么是临界区

1 每个进程中访问临界资源的那段代码称为临界区(criticalsection)
2 通过锁机制,保证每次只允许一个进程进入临界区,进入后,不允许其他进程进入

三 操作系统的各种锁

3.1 互斥锁

// 互斥锁
互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁(unlock),如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁


// 互斥锁特点
1. 原子性:把一个互斥量锁定为一个原子操作,操作系统保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;
2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量


// 注意
Python,Go,Java都支持互斥锁

3.2 自旋锁

// 自旋锁
自旋锁与互斥量功能一样,唯一一点不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁,原地打转
自旋锁在用户态使用的比较少,在内核使用的比较多!自旋锁的使用场景:锁的持有时间比较短,或者说小于2次上下文切换的时间。


// 自旋锁特点
1 某个协程持有锁时间长,等待的协程一直在循环等待,消耗CPU资源。
2 不公平,有可能存在有的协程等待时间过程,出现线程饥饿(这里就是协程饥饿)


// 注意
Python,Go不支持自旋锁
Java支持自旋锁

Go实现自旋锁

/*
1 锁也是1个变量,初值设为0;
2 1个协程将锁原子性的置为1;
3 操作变量n;
4 操作完成后,将锁原子性的置为0,释放锁。
在1个协程获取锁时,另一个协程一直尝试,直到能够获取锁(不断循环),这就是自旋锁
*/

import (
    "sync/atomic"
    "time"
)
// Spin是一个锁变量,实现了Lock和Unlock方法
type Spin int32
func (l *Spin) Lock() {
    // 原子交换,0换成1
    for !atomic.CompareAndSwapInt32((*int32)(l), 0, 1) {}
}
func (l *Spin) Unlock() {
    // 原子置零
    atomic.StoreInt32((*int32)(l), 0)
}
type Locker interface {
    Lock()
    Unlock()
}
func main() {
    var l Locker
    l = new(Spin)
    var n int
    // 两个协程
    for i := 0; i < 2; i++ {
        go routine(i, &n, l, 200*time.Millisecond)
    }
    select {}
}
func routine(i int, v *int, l Locker, d time.Duration) {
    // 实现自旋加锁
    for {
        func() {
            l.Lock()
            defer l.Unlock()
            *v++
            println(*v, i)
            time.Sleep(d)
        }()
    }
}

3.3 可重入锁(递归锁)

// 可重入锁
为了解决互斥锁导致的死锁问题(哲学家吃面问题),引入可重入锁又叫递归锁
可重入内部维护着一个锁和一个计数器,计数器记录了获取锁的次数,从而使得资源可以被同一个线程多次获取,直到一个线程所有的获取都被释放,其他的线程才能获得资源


// 注意
Go不支持可重入锁 //https://blog.csdn.net/qq_39397165/article/details/117433641
Python,Java支持可重入锁

3.4 读写锁

// 读写锁
读写锁允许更改的并行性,写的串行性,也叫共享互斥锁。
互斥量要么是锁住状态,要么就是不加锁状态,而且一次只有一个线程可以对其加锁。
读写锁可以有3种状态:读模式下加锁状态、写模式加锁状态、不加锁状态。
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁(允许多个线程读但只允许一个线程写)


// 读写锁特点
1 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作;
2 如果有其它线程写数据,则其它线程都不允许读、写操作


// 注意
Python不支持读写,自行实现:https://www.cnblogs.com/LuoboLiam/p/15338632.html
Java,Go支持读写锁

3.5 信号量(Semaphore)

// 信号量
信号量可以理解为多把锁,同时允许多个线程来更改数据
信号量是一个计数器,可以用来控制多个进程对共享资源的访问
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问

// 注意
Go不支持信号量,可以自行实现:https://studygolang.com/articles/25382?fr=sidebar
Python,Java支持信号量

3.6 条件变量(Condition)

// 条件变量
线程等待,只有满足某条件时,n个线程才执行
条件变量用来自动阻塞线程,直到某特殊情况发生为止。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
一个线程等待"条件变量的条件成立"而挂起;
另一个线程使 “条件成立”(给出条件成立信号)


// 注意
Go,Python,Java都支持条件变量

3.7 其他

// 公平锁 / 非公平锁
1 公平锁:是指多个线程按照申请锁的顺序来获取锁。
2 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象

// 可重入锁 / 不可重入锁
1 可重入锁:指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
2 不可重入锁:与可重入锁相反,不可递归调用,递归调用就发生死锁。

// 独享锁 / 共享锁
1 独享锁:该锁每一次只能被一个线程所持有。
2 共享锁:该锁可被多个线程共有

// 互斥锁 / 读写锁
1 互斥锁:在访问共享资源之前对进行加锁操作,在访问完成之后进行解锁操作。 加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前进程解锁
2 读写锁:既是互斥锁,又是共享锁,read模式是共享,write是互斥(排它锁)的

// 分段锁
分段锁: 其实是一种锁的设计,并不是具体的一种锁
容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争

// 偏向锁 / 轻量级锁 / 重量级锁
1 偏向锁:是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
轻量级
2 轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁
3 重量级锁: 是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低

四 乐观锁/悲观锁

// 悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁

//乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁

五 分布式锁

在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,在这种情况下,就需要用到分布式锁

为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用并发处理相关的功能进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁

// 分布式锁的多种实现方式
https://www.cnblogs.com/liuqingzheng/p/11080501.html
posted @ 2022-03-29 03:13  刘清政  阅读(1689)  评论(0编辑  收藏  举报