> For the complete documentation index, see [llms.txt](https://narutohyc.gitbook.io/jdk/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://narutohyc.gitbook.io/jdk/duo-xian-cheng/duo-xian-cheng-suo.md).

# 多线程\_锁

![img](https://3937520527-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lz_ry1cyy2sKkwvpUnh%2Fuploads%2Fgit-blob-28ff57735c830d0cdf0fab53746c808fdcba1bbf%2F%E5%BC%82%E4%B8%96%E7%95%8C%E8%95%BE%E5%A7%86_0.jpg?alt=media)

\[TOC]

***

## 锁概念

### 锁的分类

> [如何理解互斥锁、条件锁、读写锁以及自旋锁？](https://www.zhihu.com/question/66733477/answer/1914795654)
>
> [5000字 | 24张图带你彻底理解21种并发锁](https://cloud.tencent.com/developer/article/1690909)

![img](https://3937520527-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lz_ry1cyy2sKkwvpUnh%2Fuploads%2Fgit-blob-a1e7c4d264ce557bdbd8253f03c1a85f17ffe82b%2Fv2-cb3600216f64bfbc4cb26b58af952c57_720w.png?alt=media)

#### **互斥锁和自旋锁**

互斥锁和自旋锁是最底层的两种锁，其他的很多锁都是基于他们的实现。

当线程A获取到锁后，线程B再去获取锁，有两种处理方式:

1. 线程B循环的去尝试获取锁，直到获取成功为止即**自旋锁**
2. 线程B放弃获取锁，在锁空闲时，等待被唤醒，即**互斥锁**

> 互斥锁

互斥锁会释放当前线程的cpu，导致加锁的代码阻塞，直到线程再次被唤醒。互斥锁加锁失败时，会从用户态陷入到内核态，让内核帮我们切换线程，存在一定的性能开销

```java
while (抢锁(lock) == 没抢到) {睡眠直到锁状态改变再唤醒(lock)}
```

* 当线程加锁失败，内核会把线程的状态由“运行”设置为“睡眠”，让出cpu
* 当锁空闲时，内核唤醒线程，状态设置为“就绪”，获取cpu执行

> 自旋锁

自旋锁会自**用户态**由应用程序完成，不涉及用户态到内核态的转化，没有线程上下文切换，性能相对较好，自旋锁加锁过程:

```java
while (抢锁(lock) == 没抢到) {}
```

* 查看锁的状态
* 锁空闲，获取锁，否则执行(1)

自旋锁会利用cpu一直工作直到获取到锁，中间不会释放cpu，但如果被锁住的代码执行时间较长，导致cpu空转，浪费资源

#### **读写锁**

**读写锁**由读锁和写锁组成：

* 读锁又称为共享锁，S锁
* 写锁又称为排它锁、X锁，在mysql的事务中大量使用

写锁是独占锁，一旦线程获取写锁，其他线程不能获取写锁和读锁。

读锁是共享锁，当线程获取读锁，其他线程可以获取读锁不能获取写锁。因为并发数据读取并不会改变共享数据导致数据不一致。读写锁把对共享资源的读操作和写操作分别加锁控制，能够提高读线程的并发性，适用于读多写少的场景。

#### 读优先锁、写优先锁、公平读写锁

> 读优先锁

读优先锁希望的是读锁能够被更多的线程获取，可以提高读线程的并发性。线程A获取了读锁，线程B想获取写锁，此时会被阻塞，线程c可以继续获取读锁，直到A和c释放锁，线程B才可以获取写锁。如果有很多线程获取读锁，且加锁的代码执行时间很长，就到导致线程B永远获取不到写锁。

> 写优先锁

写优先锁希望的是写锁能够被优先获取。线程A获取了读锁，线程B想获取写锁，此时会被阻塞，后面获取读锁都会失败，线程A释放锁，线程B可以获取写锁，其他获取读锁的线程阻塞。如果有很多写线程获取写锁，且加锁的代码执行时间很长，就到导致读线程永远获取不到读锁。

> 公平读写锁

上面两种锁都会造成【饥饿】现象，为解决这种问题，可以增加一个队列，把获取锁的线程（不管是写线程还是读线程）按照先进先出的方式排队，每次从队列中取出一个线程获取锁，这种获取锁的方式是公平的。

#### **乐观锁和悲观锁**

> 乐观锁

乐观锁是先修改共享资源，再用历史数据和当前数据比对验证这段时间共享数据有没有被修改，如果没有被修改，那么更新数据，如果有其他线程更新了共享资源，需要重新获取数据，再更新，验证，循环往复的重试，直到更新成功。所以**当数据更新操作比较频繁，数据冲突的概率就会比较大，重试的次数就会多，浪费CPU资源**。 乐观锁其实全程没有加锁，也叫无锁编程，所以针对读多写少的场景，并发性能较高，典型的实现MVCC，mysql中会使用MVCC构建一致性读来保证可重复读。

> 悲观锁

悲观锁是在访问共享资源之前**统统加锁**。**当并发冲突概率较高时**，乐观锁不再适用，**悲观锁就派上用场**。互斥锁、自旋锁都是悲观锁的实现。

### 惊群问题

> [\[框架\]高并发中的惊群效应](https://blog.csdn.net/second60/article/details/81252106)

当你往一群鸽子中间扔一块食物，虽然最终只有一个鸽子抢到食物，但所有鸽子都会被惊动来争夺，没有抢到食物的鸽子只好回去继续睡觉， 等待下一块食物到来。这样，每扔一块食物，都会惊动所有的鸽子，即为惊群。

简单地说：就是扔一块食物，所有鸽子来抢，但最终只一个鸽子抢到了食物。 语义分析：食物只有一块，最终只有一个鸽子抢到，但是惊动了所有鸽子，每个鸽子都跑过来，消耗了每个鸽子的能量。（这个很符合达尔文的进化论，物种之间的竞争，适者生存。）

> 操作系统的惊群

在多进程/多线程等待同一资源时，也会出现惊群。即当某一资源可用时，多个进程/线程会惊醒，竞争资源。这就是操作系统中的惊群。

1. 惊醒所有进程/线程，导致n-1个进程/线程做了**无效的调度**，上下文切换，cpu瞬时增高
2. 多个进程/线程争抢资源，所以涉及到同步问题，需对资源进行加锁保护，**加解锁加大系统CPU开销**

#### 惊群的几种情况

在高并发（多线程/多进程/多连接）中，会产生惊群的情况有：

1. accept惊群
2. epoll惊群
3. nginx惊群
4. 线程池惊群

> 线程池惊群

在多线程设计中，经常会用到互斥和条件变量的问题。当一个线程解锁并通知其他线程的时候，就会出现惊群的现象。

pthread\_mutex\_lock/pthread\_mutex\_unlock：线程互斥锁的加锁及解锁函数。 pthread\_cond\_wait：线程池中的消费者线程等待线程条件变量被通知； pthread\_cond\_signal/pthread\_cond\_broadcast：生产者线程通知线程池中的某个或一些消费者线程池，接收处理任务； 这里的惊群现象出现在3里，pthread\_cond\_signal，语义上看，是通知一个线程。调用此函数后，系统会唤醒在相同条件变量上等待的一个或多个线程（可参看手册）。如果通知了多个线程，则发生了惊群。

正常的用法：

1. 所有线程共用一个锁，共用一个条件变量
2. 当pthread\_cond\_signal通知时，就可能会出现惊群

> 解决惊群的方法：

1. 所有线程共用一个锁，每个线程有自已的条件变量
2. pthread\_cond\_signal通知时，定向通知某个线程的条件变量，不会出现惊群
