当前位置: 首页 > news >正文

【linux】线程互斥

线程互斥

  • 1.线程互斥
  • 2.可重入VS线程安全
  • 3.常见锁的概念

在这里插入图片描述

喜欢的点赞,收藏,关注一下把!在这里插入图片描述

1.线程互斥

到目前为止我们学了线程概念,线程控制接下来我们进行下一个话题,线程互斥。

有没有考虑过这样的一个问题,既然线程一旦被创建,几乎所有资源都是被所有线程共享的。 那多个线程访问同一份共享资源有没有什么问题?

下面我们模拟一下抢票的场景,看到底有没有什么问题

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>//共享资源,火车票
int ticket = 10000;void *GetTicket(void *args)
{string name = static_cast<const char *>(args);while (true){if (ticket > 0){cout << name << " 正在进行抢票: " << ticket << endl;ticket--;//以微秒为单位进行休眠,模拟真实的抢票要花费的时间usleep(1000);}else{break;}}
}int main()
{pthread_t t1,t2,t3,t4;pthread_create(&t1,nullptr,GetTicket,(void*)"thread->1");pthread_create(&t2,nullptr,GetTicket,(void*)"thread->2");pthread_create(&t3,nullptr,GetTicket,(void*)"thread->3");pthread_create(&t4,nullptr,GetTicket,(void*)"thread->4");pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);return 0;
}

在这里插入图片描述
这好像没出问题啊,到0就停止了。

我们想看到的现象是抢到负数票,那怎么实现呢?
既然想看到抢到负数票,就需要尽可能的让多个线程交叉执行。
多个线程交叉执行的本质:就是让调度器尽可能的频繁发生线程调度与切换。
线程一般在时候发生切换呢?
时间片到了,来了更高优先级的线程,线程等待的时候。
那线程是什么时候检测上面的问题呢?
从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换。

那修改一下代码

void *GetTicket(void *args)
{string name = static_cast<const char *>(args);while (true){if (ticket > 0){//线程进来之后先休眠,要被切走usleep(1000);cout << name << " 正在进行抢票: " << ticket << endl;ticket--;//以微秒为单位进行休眠,模拟真实的抢票要花费的时间//usleep(1000);}else{break;}}
}

在这里插入图片描述
出问题了,放了10000张票,结果抢到了10002张票。现象就是这个样子。那为什么会出现这样的问题?

在这里插入图片描述
所谓判断的本质逻辑:
1.读取内存数据到CPU内部寄存器中
2.进行判断

所以ticket=1,多个线程可以同时执行这个判断语句。对不对?
答案是不对的。
我们只有一个CPU,只有一份寄存器,不能同时判断,但是注意我们写了usleep语句,线程是要被切换走的,但是寄存器中的内容是属于这个线程的,因此1也要被切走
在这里插入图片描述
剩下的线程就可以开始竞争执行if语句判断,但很不幸最终都是进去后先休眠。
在这里插入图片描述
当线程1被唤醒恢复上下文,执行到ticket- -;
ticket- -有三个步骤:1.读取数据,2.更改数据,3.写回数据
此时线程1从内存中读取ticke还是1,最后写回内存ticket为0
在这里插入图片描述
线程2此时醒来恢复上下文,它不知道内存中ticket此时是0,往下执行到ticket- -,此时取到ticket是0,最后写回内存中ticket是-1
在这里插入图片描述
线程3也醒来,和上面一样,执行到ticket- -,从内存取到数据为-1,写回内存中是-2
在这里插入图片描述
就是因为我们判断和更新分开,而在中间发生了大量的线程切换,最终可能出现ticket本来就是1了,但是你却放了大量线程同时进来对ticket变量做减减操作,进而导致我们的数据出现了负数的情况

那没有判断,多线程单纯对一个全局变量进行修改是安全的吗?
假设ticket初始是1000,多个线程进行执行ticket- -操作

在这里插入图片描述
假设刚开始threadA执行。
补充:
对变量进行++,或者- - ,在C/C++上看起来只有一条语句,但是汇编之后至少是三条语句
1.从内存读取数据到CPU寄存器中
2.在寄存器中让CPU进行对应的算逻运算
3.写回到新的结果到内存中变量的为止
未来会对应三条汇编语句!!

threadA,做完第1,2步ticket变成999了,准备执行第3步,
在这里插入图片描述
但不幸的是threadA被切走了,虽然被切走了,但是等会回来还是从被切走的地方继续往下执行。寄存器只有一份,但寄存器的内容属于当前进程的上下文,threadA被切走了,自然这些东西也被要拿走到threadA的上下文
在这里插入图片描述
threadB被调度 ,threadB是新来的,它要重新开始执行这个语句。从1000开始减。第1,2,3步一直在疯狂执行。执行了800次,当这一次ticket写回到内存变成200后,再次执行到第一步,
在这里插入图片描述
时间片到了就把threadB切换走了。
在这里插入图片描述
然后把thradA拿回来了,首先恢复它的上下文,它的寄存器放的依旧还是999,然后继续执行第3步。
在这里插入图片描述
但是这一下就完蛋了,threadB好不容易把ticket减到了200,你一下给人感到了999。此时是不是相当于多线程运算的时候发生了干扰问题。

所以即便没有上面if,只有- -依旧出现问题。

我们得到的结论就是:我们定义的全局变量,在没有保护的时候,往往是不安全的,像上面多个线程在交替执行造成的数据安全问题,发生了数据不一致问题。

下一步就是解决这个问题。如何解决呢?
加锁

在解释之前,我们先复盘一下以前学过的知识。
1.多个执行流进行安全访问的共享资源 ---- 临界资源
2.我们把多个执行流中,访问临界资源的代码 ---- 临界区
(并不是整个代码都是临界区,只有访问临界资源的代码才是临界区)
临界区往往是线程代码中很小的一部分
在这里插入图片描述
3.想让多个线程串行访问共享资源 ---- 互斥
像刚刚的情况就是我们多线程在并发或并行的访问而没有保护而所导致出现的问题。
4.对一个资源进行访问的时候,要么不做,要么做完 ---- 原子性 理解它,我们看看不是原子性的情况

在这里插入图片描述
就比如这个,执行第1,2步,但到第3步的是被切走了,请问thradA对ticket- -操作是原子的吗?
不是,虽然它做了,但是没做完,有中间状态,第一步,第二步,第三步是可以被打断了的,这就不是原子性。虽然ticket- -只是一句语句,但是未来会对应三条汇编语句。

我们给原子性一个的概念
一个对资源进行的操作,如果只用一条汇编就能完成 ---- 原子性
反之:就不是原子的
当前这样理解是为了方便表述,所以这样理解。但是假设原子性是一个圈,我们刚说的这个只是原子性的一个子集。

接下来我们细谈这把锁。
锁也是一个数据类型,它的类型是

pthread_mutex_t

如果一个锁定义好了,我们必须对它进行初始化
在这里插入图片描述
如果是把局部锁,就必须用init初始,用完之后destroy销毁
在这里插入图片描述
mutex:要初始化的锁
attr:锁的属性,我们设置成nullptr就可以了

全局的锁,只需这样就自动初始化,销毁
在这里插入图片描述
而使用锁想对某段代码进行安全访问,必须加锁
在这里插入图片描述
未来不想保护了,就解锁
在这里插入图片描述
我们用用这把锁

int ticket = 10000;//全局锁
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;void *GetTicket(void *args)
{string name = static_cast<const char *>(args);while (true){//加锁pthread_mutex_lock(&lock);if (ticket > 0){usleep(1000);cout << name << " 正在进行抢票: " << ticket << endl;ticket--;//以微秒为单位进行休眠,模拟真实的抢票要花费的时间//usleep(1000);pthread_mutex_unlock(&lock);//解锁}else{//加锁可能条件不满足走到else,这里也需要解锁pthread_mutex_unlock(&lock);break;}//不能这里解锁,不然if条件不满足,走到else直接跳出循环还没有解锁//pthread_mutex_unlock(&lock);}
}

在加锁和解锁之间的就是传说中的临界资源,而访问临界资源的代码就是临界区,而通过加锁解锁也保证这部分代码要么不做要做完成。
在这里插入图片描述

局部锁我们也看看怎么用

int ticket = 10000;//全局锁
//pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;//又想把"thread->1"参数给GetTicket,也想把锁给它
class ThreadData
{
public:ThreadData(pthread_mutex_t* mutex_p,const string& name):_mutex_p(mutex_p),_threadname(name){}~ThreadData(){}
public:pthread_mutex_t* _mutex_p;string _threadname;
};void *GetTicket(void *args)
{ThreadData* td=static_cast<ThreadData*>(args);while (true){//加锁pthread_mutex_lock(td->_mutex_p);if (ticket > 0){usleep(1000);cout <<  td->_threadname<< " 正在进行抢票: " << ticket << endl;ticket--;//以微秒为单位进行休眠,模拟真实的抢票要花费的时间//usleep(1000);pthread_mutex_unlock(td->_mutex_p);//解锁}else{//加锁可能条件不满足走到else,这里也需要解锁pthread_mutex_unlock(td->_mutex_p);break;}}
}int main()
{//局部锁pthread_mutex_t lock;pthread_mutex_init(&lock,nullptr);//使用前初始化
#define NUM 4vector<pthread_t> tids(NUM);for (int i = 0; i < NUM; ++i){char namebuffer[64];snprintf(namebuffer, sizeof(namebuffer), "thread->%d", i + 1);//锁和threadname都传给函数ThreadData* td=new ThreadData(&lock,namebuffer);pthread_create(&tids[i], nullptr, GetTicket, td);}for (auto &thread : tids){pthread_join(thread, nullptr);}pthread_mutex_destroy(&lock);//使用后销毁return 0;
}

在这里插入图片描述
运行结果是,我们的程序变慢了,这是为什么?(这里图片看不出来。
)。并且只有一个线程在抢?

加锁和解锁的过程是多个线程串行执行的,所以程序变慢了。
只有一个线程在抢的原因是:锁只规定互斥访问,没有规定必须让谁先优先执行,锁就是真是的让多个执行流进行竞争的结果。为什么一直是这个线程在跑因为它竞争锁的能力比其他线程强。

现在问题是,一般抢完票就完了吗?
当然不是,我们所看到票的信息,都是已经处理完之后给我们显示的。
所以一个线程抢完票之后还要做其他事情,这样才有机会让其他线程有持有锁的可能性

void *GetTicket(void *args)
{//string name = static_cast<const char *>(args);ThreadData* td=static_cast<ThreadData*>(args);while (true){//加锁pthread_mutex_lock(td->_mutex_p);if (ticket > 0){usleep(1000);cout <<  td->_threadname<< " 正在进行抢票: " << ticket << endl;ticket--;//以微秒为单位进行休眠,模拟真实的抢票要花费的时间//usleep(1000);pthread_mutex_unlock(td->_mutex_p);//解锁}else{//加锁可能条件不满足走到else,这里也需要解锁pthread_mutex_unlock(td->_mutex_p);break;}//模拟处理其他事情usleep(1);}
}

在这里插入图片描述

接下来我们思考几个问题

1.如何看待锁

ticket是一个全局变量,被多个线程同时访问,我们称这个变量为共享资源或全局资源,这个共享资源经过锁的保护变成了临界资源,临界资源可以保证我们进行安全的访问,这个没什么问题。
那这把锁是不是也一定是要被多个线程访问临界资源前要先访问这把锁。因为要保护共享资源,所以要先加锁,要加锁是不是得每个线程都先看到并访问这把锁。
a.锁,本身就是一个共享资源!
全局的变量是要被保护的,而锁是用来保护全局的资源的。锁本身也是全局资源,锁的安全谁来保护呢?
b.pthread_mutex_lock、pthread_mutex_unlock:加锁的过程必须是安全的!
那是如何设计的呢?
加锁的过程其实是原子的! 要么就申请到,要么就不申请,不会存在中间状态。
c.如果申请成功,就继续向后执行,如何申请暂时没有成功,执行流会如何?

void *GetTicket(void *args) 
{//string name = static_cast<const char *>(args);ThreadData* td=static_cast<ThreadData*>(args);while (true){//加锁pthread_mutex_lock(td->_mutex_p);pthread_mutex_lock(td->_mutex_p);//申请一次,再申请一次if (ticket > 0){usleep(1000);cout <<  td->_threadname<< " 正在进行抢票: " << ticket << endl;ticket--;//以微秒为单位进行休眠,模拟真实的抢票要花费的时间//usleep(1000);pthread_mutex_unlock(td->_mutex_p);//解锁}else{//加锁可能条件不满足走到else,这里也需要解锁pthread_mutex_unlock(td->_mutex_p);break;}//模拟处理其他事情usleep(1);}
}

在这里插入图片描述
我们看到代码就不再运行了,多线程每一个线程都在,但都不跑了
在这里插入图片描述
如果申请暂时没有成功,执行流会阻塞!(相当于挂起的状态) 去休眠了,直到这个锁释放了,OS或库会自动唤醒这个线程让它继续向后执行。

d.谁持有锁,谁进入临界区!

上面我们把临界资源,临界区,互斥都说过了,现在也都能理解了,可是原子性,为了理解给上面的解锁,只不过是一个子集。下面我们再把原子性问题谈一谈。
在这里插入图片描述
这个问题很好回答,刚说的,其他线程只能阻塞等待!
在这里插入图片描述
绝对可以的!随便切!
当持有锁的线程被迫切走的时候,是抱着锁被切走的,即便自己被切走了,其他线程依旧无法申请成功,也便无法向后执行!直到我最终释放这个锁!

在这里插入图片描述
所以,对于其他线程而言,有意义锁的状态,无非两种
1.申请锁前
2.释放锁后
而站在其他线程的角度,看待当前线程持有锁的过程,就是原子的!

在未来我们使用锁的时候,一定要尽量保证临界区的粒度(锁中间保护代码的多少)要非常小。因为是串行访问的。

并且如果让线程1,线程2访问公共资源就加锁串行起来,线程3不加锁直接访问,这样是不对的。加锁是程序员行为,必须做到要加就都要加!

2.如何理解加锁和解锁的本质
加锁的过程是原子的!
其实你注意会发现解锁的要求其实并不高,我加锁了未来肯定只有一个执行流在解锁。解锁这件事情对原子性的要求或者安全性要求并不是特别特别强,但我们依旧要保证它的原子性,库也是这样设计的。

接下来我们就看看加锁解锁的实现原理,是如何保证原子性的。

到现在我们已经发现单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据不一致性问题。
为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
在这里插入图片描述

在继续往下谈的时候,我们先建立一个共识
1.CPU内寄存器只有一套被所有执行流共享
2.CPU内寄存器的内容,是每个执行流私有的,称之为运行时上下文。

线程A先跑,要进行加锁,加锁的伪代码就是lock下面的语句,
线程A先把0放到%al寄存器中
在这里插入图片描述
此时线程A把0放到寄存器中,可以被切走吗?在加锁任何一条汇编执行前或执行后,任何一条语句线程都可以被切换。
现在问题是把0放到%al中,这条语句的本质是什么?
是不是相当于把0值放到线程A的上下文当中!
如果这个时候线程A被切走,线程A一点都不担心,因为它知道我要被切走时,我要把这个0也要带走,当我回来时我在把0放回来。

接下来执行下一句汇编
在这里插入图片描述
内存中的mutex变量,线程A可以访问,线程B也可以访问,那这个mutex变量不就是我们的共享变量吗,就如同ticket,而刚刚竟然用一条汇编让线程A将寄存器中的值和内存中的值做交换,这个交换的动作是一条汇编完成,交换的本质是什么?
交换的本质:共享的数据,交换到我的上下文中!!!
就相当于线程A把这个锁拿走了,此时刚把这条语句执行完,线程A就被切走了,切走了线程A怕不怕,一点都不怕,先别着急把我切走,我要把我的上下文带走。
在这里插入图片描述
线程B被调度,依旧要执行加锁的逻辑,首先把0放到%al寄存器中
在这里插入图片描述
接下来也要进行交换,可是现在交换是0换0,线程B此时申请锁就不成功了
在这里插入图片描述

不成功,线程B只能挂起等待了。
线程A只要申请成功了,即使后序被切走了,但是一点都不担心,因为它是持有锁被切走的,其他线程来也申请不到锁。
线程B被切走了把0也带走,OS又调度线程A然后恢复上下文把1放到%al里,继续向下执行,经过if判断申请锁成功返回,执行自己的语句
在这里插入图片描述
在这里插入图片描述
解锁的代码就特别简单了,一句mov把1拷贝到mutex,唤醒其他等待锁的线程,然后return就结束了
在这里插入图片描述
其他线程就可以以同样的逻辑进行加锁解锁。

3.如果我们想简单的使用,该如何进行封装设计
把这个锁封装起来

//Mutex.hpp
#pragma once
#include<iostream>
#include<pthread.h>using namespace std;class Mutex
{
public:Mutex(pthread_mutex_t* lock_p=nullptr):_lock_p(lock_p){}void lock(){if(_lock_p) pthread_mutex_lock(_lock_p);}void unlock(){if(_lock_p) pthread_mutex_unlock(_lock_p);}~Mutex(){}private:pthread_mutex_t* _lock_p;
};class LockGuard
{
public:LockGuard(pthread_mutex_t* lockp):_mutex(lockp){_mutex.lock();//在构造函数中进行加锁}~LockGuard(){_mutex.unlock();//在析构函数中进行解锁}private:Mutex _mutex;
};//mythread.cc
#include <unistd.h>
#include <vector>
#include "Mutex.hpp"int ticket = 10000;// 又想把"thread->1"参数给GetTicket,也想把锁给它
class ThreadData
{
public:ThreadData(pthread_mutex_t *mutex_p, const string &name): _mutex_p(mutex_p), _threadname(name){}~ThreadData(){}public:pthread_mutex_t *_mutex_p;string _threadname;
};void *GetTicket(void *args)
{// string name = static_cast<const char *>(args);ThreadData *td = static_cast<ThreadData *>(args);while (true){{//构造的时候自动加锁,后面的代码都处于加锁状态//局部变量生命周期随作用域,也就是这个代码块//一次循环结束后自动调用析构函数,也就是自动解锁了//这里不想把模拟其他事情也加锁,不然都是一个线程再跑,因此把加锁代码单独弄个作用域LockGuard lockguard(td->_mutex_p);if (ticket > 0){usleep(1000);// cout << name<< " 正在进行抢票: " << ticket << endl;cout << td->_threadname << " 正在进行抢票: " << ticket << endl;ticket--;}else{break;}}// 模拟处理其他事情usleep(1000);}
}int main()
{// 局部锁pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr); // 使用前初始化#define NUM 4vector<pthread_t> tids(NUM);for (int i = 0; i < NUM; ++i){char namebuffer[64];snprintf(namebuffer, sizeof(namebuffer), "thread->%d", i + 1);ThreadData *td = new ThreadData(&lock, namebuffer);pthread_create(&tids[i], nullptr, GetTicket, td);}for (auto &thread : tids){pthread_join(thread, nullptr);}pthread_mutex_destroy(&lock);return 0;
}

就我们刚刚写的代码,票都被一个线程抢了,这个线程错了吗?
它没错,它本来就是互斥安全的访问,但并不合理。造成了其他线程的饥饿状态,在继续往下学习中,我们先谈一点概念。

2.可重入VS线程安全

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程不安全的情况:

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全的情况:

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况:

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见可重入的情况:

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

  • 可重入函数是线程安全函数的一种(线程一般是要调函数的,构造析构等等)
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

3.常见锁的概念

这里我们就说一种
死锁

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

具体来说就是指一组执行流不管是线程还是进程,它在持有自己的锁资源的同时,还想方设法的去申请对方的锁资源,因为大家互相持有自己的还申请对方的,而锁是不可抢占的锁,不可抢占就是我拿了锁除非我主动归还,否则你想要我的锁我不给。所以当我们互相持有自己的锁还想要对方的锁的时候,进而可能导致我们多执行流互相等待对方的资源而导致代码无法推进的情况。

举个例子
张三和李四这两个小朋友一起去商店,我是这家店的老板,他俩爸爸妈妈各自给他们一人五毛钱,问我棒棒糖多少钱,我说一块钱,张三问李四是不是有五毛钱,李四说是,张三说你把五毛钱给我把,我凑成一块钱买棒棒糖好不好,李四说为什么我要给你,你怎么不给我呢,你能不能把你的五毛钱给我,我凑成一块钱我来买棒棒糖。张三当然也不同意了。
这两个小朋友就互相拿着自己五毛钱,还在要着对方的五毛钱,两个人争执不下谁都没买成棒棒糖,这两个小朋友的状态就叫做死锁。

接下来我们谈谈死锁。
在多把锁的场景下,我们持有自己的锁不释放,还要对方的锁,对方也是如此,此时就容易造成死锁!

1.一把锁,有可能死锁吗?
当然有可能。
就比如说别人可以把你绊倒,你自己可以把自己绊倒吗?当然是可以的。
在这里插入图片描述
2.为什么会有死锁?我们看看逻辑链条

一定是你用了锁<—为什么你要用锁呢?<—保证临界资源的安全<----为什么要保证临界资源的安全?<—多线程访问我们可能会出现数据不一致的问题<—为什么会出现数据不一致的问题?<—因为是我们多线程并且使用的是全局资源<—为什么多线程访问全局资源会造成这样的问题<—多线程大部分资源(全局的)是共享的<—多线程共性

任何技术都有自己的边界,是解决问题的,但是可能在解决问题的同时,一定可能会引入新的问题!

死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

这是锁的四个必要条件可能还会有其他条件,但是只要你需要得到死锁,这四个条件必须同时满足。

互斥:很好理解,就是必须保证访问某种资源是互斥的,这个没什么问题,这个是我们锁的基本特性,你是一把锁,本身就具有互斥能力,没有互斥能力怎么能谈你是一把锁呢?

请求与保持:请求就是我要你,保持就是我不释放我的。我要你的,但我不释放我的。

不剥夺:就像刚才要五毛钱,你要不给我我就揍你这是剥夺,另一种是把五毛钱给我,你别害怕我不打你我也不抢你的,我要你自愿给我。这叫做不剥夺条件。

循环等待,A有自己的锁,它去要B的锁,B有自己的锁,它去要A的锁。这就是循环等待,刚才的张三和李四就是形成了循环等待。

一旦死锁这四个条件都必须同时满足!那只要破坏这四个条件之间的其中一个死锁就不满足了。

避免死锁

  • 破坏死锁的四个必要条件

互斥是锁的一种特性这不用考虑了没有办法破坏,你不用锁了也根本不会产生死锁问题。

请求与保持好处理,比如说我们的线程要访问一个或多个临界资源,它需要同时拥有两把锁,申请第一把锁成功,如果第二把锁申请失败了,失败了就把自己曾经申请的锁释放掉。此时就不会造成死锁了。

不剥夺,所谓不剥夺就是不能抢,那我们可以设置一个竞争策略,比如A申请到锁,再去申请B的锁,B的锁被其他线程拿到了,这个时候我们比较定义的优先级或其他,假设A的优先级比较高,那拿到B的锁的线程必须主动释放锁。

循环等待,就相当于我们在申请锁的时候,A线程先申请A锁,在申请B锁,而B线程先申请B锁,在申请A锁,所以两个线程天然申请锁的顺序就是环状的。我们可以尽量不让线程出现这个环路情况,我们让两个线程申请锁的顺序保持一致,就可以破坏循环等待问题。两个线程都是先申请A锁在申请B锁。

  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

资源一次性分配,比如说你有一万行代码,有五处要申请锁,你可以最开始一次就给线程分配好,而不要把五处申请打散到代码各个区域里所导致加锁场景非常复杂

避免死锁算法

  • 死锁检测算法(了解)
  • 银行家算法(了解)

一个线程申请到锁,另一个线程可以解锁吗?比如说A线程申请到锁,B线程可以释放这个锁吗?

在这里插入图片描述
注意这一句可是把1拷贝到内存种mutex,可不是把线程上下文中的1拷贝过去。
所以是可以的!

相关文章:

【linux】线程互斥

线程互斥 1.线程互斥2.可重入VS线程安全3.常见锁的概念 喜欢的点赞&#xff0c;收藏&#xff0c;关注一下把&#xff01; 1.线程互斥 到目前为止我们学了线程概念&#xff0c;线程控制接下来我们进行下一个话题&#xff0c;线程互斥。 有没有考虑过这样的一个问题&#xff0c…...

机器学习原理到Python代码实现之LinearRegression

Linear Regression 线性回归模型 该文章作为机器学习的第一篇文章&#xff0c;主要介绍线性回归模型的原理和实现方法。 更多相关工作请参考&#xff1a;Github 算法介绍 线性回归模型是一种常见的机器学习模型&#xff0c;用于预测一个连续的目标变量&#xff08;也称为响应变…...

Hive SQL / SQL

1. 建表 & 拉取表2. 插入数据 insert select3. 查询3.1 查询语句语法/顺序3.2 关系操作符3.3 聚合函数3.4 where3.5 分组聚合3.6 having 筛选分组后结果3.7 显式类型转换 & select产生指定值的列 4. join 横向拼接4.1 等值连接 & 不等值连接4.2 两表连接4.2.1 内连…...

程序媛的mac修炼手册--MacOS系统更新升级史

啊&#xff0c;我这个口罩三年从未感染过新冠的天选免疫王&#xff0c;却被支原体击倒&#x1f637;大意了&#xff0c;前几天去医院体检&#xff0c;刚检查完出医院就摘口罩了&#x1f926;大伙儿还是要注意戴口罩&#xff0c;保重身体啊&#xff01;身体欠恙&#xff0c;就闲…...

【数据库原理】(9)SQL简介

一.SQL 的发展历史 起源&#xff1a;SQL 起源于 1970 年代&#xff0c;由 IBM 的研究员 Edgar F. Codd 提出的关系模型概念演化而来。初期&#xff1a;Boyce 和 Chamberlin 在 IBM 开发了 SQUARE 语言的原型&#xff0c;后发展成为 SQL。这是为了更好地利用和管理关系数据库。…...

第二百五十二回

文章目录 概念介绍实现方法示例代码 我们在上一章回中介绍了如何在页面中添加图片相关的内容&#xff0c;本章回中将介绍如何给组件添加阴影.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 我们在本章回中介绍的阴影类似影子&#xff0c;只是它不像影子那么明显&a…...

Leetcode 3701 · Find Nearest Right Node in Binary Tree (遍历和BFS好题)

3701 Find Nearest Right Node in Binary TreePRE Algorithms This topic is a pre-release topic. If you encounter any problems, please contact us via “Problem Correction”, and we will upgrade your account to VIP as a thank you. Description Given a binary t…...

网站被攻击了,接入CDN对比直接使用高防服务器有哪些优势

网站是互联网行业中经常被攻击的目标之一。攻击是许多站长最害怕遇到的情况。当用户访问一个网站&#xff0c;页面半天打不开&#xff0c;响应缓慢&#xff0c;或者直接打不开&#xff0c;多半是会直接走开&#xff0c;而不是等待继续等待相应。针对网站攻击的防护&#xff0c;…...

location常用属性和方法

目录 Location 对象 Location 对象属性 Location 对象方法 location.assign() location.replace() location.reload() Location 对象 Location 对象包含有关当前 URL 的信息。Location 对象是 Window 对象的一个部分&#xff0c;可通过 window.location 属性来访问。 L…...

二分图

目录 二分图 染色法判定二分图 匈牙利算法 二分图 二分图&#xff0c;又叫二部图&#xff0c;将所有点分成两个集合&#xff0c;使得所有边只出现在集合之间的点之间&#xff0c;而集合内部的点之间没有边。二分图当且仅当图中没有奇数环。只要图中环的边数没奇数个数的&am…...

[VUE]3-路由

目录 路由 Vue-Router1、Vue-Router 介绍2、路由配置3、嵌套路由3.1、简介3.2、实现步骤3.3、⭐注意事项 4、⭐router-view标签详解 ​&#x1f343;作者介绍&#xff1a;双非本科大三网络工程专业在读&#xff0c;阿里云专家博主&#xff0c;专注于Java领域学习&#xff0c;擅…...

Kafka(六)消费者

目录 Kafka消费者1 配置消费者bootstrap.serversgroup.idkey.deserializervalue.deserializergroup.instance.idfetch.min.bytes1fetch.max.wait.msfetch.max.bytes57671680 (55 mebibytes)max.poll.record500max.partition.fetch.bytessession.timeout.ms45000 (45 seconds)he…...

RK3399平台入门到精通系列讲解(实验篇)共享工作队列的使用

🚀返回总目录 文章目录 一、工作队列相关接口函数1.1、初始化函数1.2、调度/取消调度工作队列函数二、信号驱动 IO 实验源码2.1、Makefile2.2、驱动部分代码工作队列是实现中断下半部分的机制之一,是一种用于管理任务的数据结构或机制。它通常用于多线程,多进程或分布式系统…...

STM32 基于 MPU6050 的飞行器姿态控制设计与实现

基于STM32的MPU6050姿态控制设计是无人机、飞行器等飞行器件开发中的核心技术之一。在本文中&#xff0c;我们将介绍如何利用STM32和MPU6050实现飞行器的姿态控制&#xff0c;并提供相应的代码示例。 1. 硬件连接及库配置 首先&#xff0c;我们需要将MPU6050连接到STM32微控制…...

大数据平台Bug Bash大扫除最佳实践

一、背景 随着越来越多的"新人"在日常工作以及大促备战中担当大任&#xff0c;我们发现仅了解自身系统业务已不能满足日常系统开发运维需求。为此&#xff0c;大数据平台部门组织了一次Bug Bash活动&#xff0c;既能提升自己对兄弟产品的理解和使用&#xff0c;又能…...

JavaScript 中的数组过滤

在构建动态和交互式程序时&#xff0c;您可能需要添加一些交互式功能。例如&#xff0c;用户单击按钮以筛选一长串项目。 您可能还需要处理大量数据&#xff0c;以仅返回与指定条件匹配的项目。 在本文中&#xff0c;您将学习如何使用两种主要方法在 JavaScript 中过滤数组。…...

随机森林(Random Forest)

随机森林&#xff08;Random Forest&#xff09;是一种集成学习方法&#xff0c;通过组合多个决策树来提高模型的性能和鲁棒性。随机森林在每个决策树的训练过程中引入了随机性&#xff0c;包括对样本和特征的随机选择&#xff0c;以提高模型的泛化能力。以下是随机森林的基本原…...

本地引入Element UI后导致图标显示异常

引入方式 npm 安装 推荐使用 npm 的方式安装&#xff0c;它能更好地和 webpack 打包工具配合使用。 npm i element-ui -SCDN 目前可以通过 unpkg.com/element-ui 获取到最新版本的资源&#xff0c;在页面上引入 js 和 css 文件即可开始使用。 <!-- 引入样式 --> <…...

UE5.1_UMG序列帧动画制作

UE5.1_UMG序列帧动画制作 UMG序列帧动画制作相对比较简单&#xff0c;不像视频帧需要创建媒体播放器那么复杂&#xff0c;以下简要说明&#xff1a; 1. 事件函数 2. 准备序列帧装入数组 3. 构造调用事件函数 4. 预览 序列帧UMG0105 5. 完成&#xff01;按需配置即可。...

总结HarmonyOS的技术特点

HarmonyOS是华为自主研发的面向全场景的分布式操作系统。它的技术特点主要体现在以下几个方面&#xff1a; 分布式架构&#xff1a;HarmonyOS采用了分布式架构设计&#xff0c;通过组件化和小型化等方法&#xff0c;支持多种终端设备按需弹性部署&#xff0c;能够适配不同类别的…...

从0到1入门C++编程——04 类和对象之封装、构造函数、析构函数、this指针、友元

文章目录 一、封装二、项目文件拆分三、构造函数和析构函数1.构造函数的分类及调用2.拷贝函数调用时机3.构造函数调用规则4.深拷贝与浅拷贝5.初始化列表6.类对象作为类成员7.静态成员 四、C对象模型和this指针1.类的对象大小计算2.this指针3.空指针访问成员函数4.const修饰成员…...

Robot Operating System 2: Design, Architecture, and Uses In The Wild

Robot Operating System 2: Design, Architecture, and Uses In The Wild (机器人操作系统 2&#xff1a;设计、架构和实际应用) 摘要&#xff1a;随着机器人在广泛的商业用例中的部署&#xff0c;机器人革命的下一章正在顺利进行。即使在无数的应用程序和环境中&#xff0c;也…...

TinyEngine 服务端正式开源啦!!!

背景介绍 TinyEngine 低代码引擎介绍 随着企业对于低代码开发平台的需求日益增长&#xff0c;急需一个通用的解决方案来满足各种低代码平台的开发需求。正是在这种情况下&#xff0c;低代码引擎应运而生。它是一种通用的开发框架&#xff0c;通过对低代码平台系统常用的功能进…...

网页设计与制作web前端设计html+css+js成品。电脑网站制作代开发。vscodeDrea 【企业公司宣传网站(HTML静态网页项目实战)附源码】

网页设计与制作web前端设计htmlcssjs成品。电脑网站制作代开发。vscodeDrea 【企业公司宣传网站&#xff08;HTML静态网页项目实战&#xff09;附源码】 https://www.bilibili.com/video/BV1Hp4y1o7RY/?share_sourcecopy_web&vd_sourced43766e8ddfffd1f1a1165a3e72d7605...

Avalonia学习(二十)-登录界面演示

今天开始继续Avalonia练习。 本节&#xff1a;演示实现登录界面 在网上看见一个博客&#xff0c;展示Avalonia实现&#xff0c;仿照GGTalk&#xff0c;我实现了一下&#xff0c;感觉是可以的。将测试的数据代码效果写下来。主要是样式使用&#xff0c;图片加载方式。 只有前…...

Spring依赖注入的魔法:深入DI的实现原理【beans 五】

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 Spring依赖注入的魔法&#xff1a;深入DI的实现原理【beans 五】 前言DI的基本概念基本概念&#xff1a;为什么使用依赖注入&#xff1a; 构造器注入构造器注入的基本概念&#xff1a;示例&#xff1a…...

【学习笔记】1、数字逻辑概论

1.1 数字信号 数字信号&#xff0c;在时间和数值上均是离散的。数字信号的表达方式&#xff1a;二值数字逻辑和逻辑电平描述的数字波形。 &#xff08;1&#xff09; 数字波形的两种类型 数值信号又称为“二值信号”。数字波形又称为“二值位形图”。 什么是一拍 一定的时…...

设置代理IP地址对网络有什么影响?爬虫代理IP主要有哪些作用?

在互联网的广泛应用下&#xff0c;代理IP地址成为了一种常见的网络技术。代理IP地址可以改变用户的上网行为&#xff0c;进而影响网络访问的速度和安全性。本篇文章将探讨设置代理IP地址对网络的影响&#xff0c;以及爬虫代理IP的主要作用。 首先&#xff0c;让我们来了解一下代…...

聊聊jvm的mapped buffer的统计

序 本文主要研究一下jvm的mapped buffer的统计 示例 private void writeDirectBuffer() {// 分配一个256MB的直接缓冲区ByteBuffer buffer ByteBuffer.allocateDirect(256 * 1024 * 1024);// 填充数据Random random new Random();while (buffer.remaining() > 4) {buff…...

matrix-breakout-2-morpheus 靶场 练习思路

arp-scan -l 获取目标机器的IP nmap -sV -A IP 查看目标机器开放的端口 gobuster dir -u http://192.168.29.130 -x php,txt,jsp,asp -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt 爆破目标机器的文件目录,找到可以访问的文件路径 http://192.168…...

手机怎么制作h5作品/搜索引擎优化的作用是什么

。。。。认真吧。。。转载于:https://www.cnblogs.com/lalawu/p/3553173.html...

外省公司做网站备案/免费的云服务器有哪些

1 案例1&#xff1a;加密与解密应用 1.1 问题 本案例要求采用gpg工具实现加/解密及软件签名等功能&#xff0c;分别完成以下任务&#xff1a; 检查文件的MD5校验和 使用GPG实现文件机密性保护&#xff0c;加密和解密操作 使用GPG的签名机制&#xff0c;验证数据的来源正确性123…...

做购物比价的网站/网址ip地址查询工具

原文链接&#xff1a;https://cloud.google.com/kubernetes-engine/kubernetes-comic/后台回复“加群”&#xff0c;带你进入高手如云交流群▼喜欢&#xff0c;就给我一个“在看”10T 技术资源大放送&#xff01;包括但不限于&#xff1a;云计算、虚拟化、微服务、大数据、网络…...

香港网站建设/crm管理系统

1.主对角元素为a,其他元素全为b,|A|[a(n-1)b] (a-b)^(n-1) 2.每行元素和为常数&#xff0c;则这个常数就是一个特征值&#xff0c;特征向量为(1,1,1,1,1) 3.若r(A)1&#xff0c;则矩阵的一个特征值为tr(A),其他为零 4.行列式为零&#xff0c;零是一个特征值 5.实对称矩阵对…...

做网站有什么必要/口碑营销策划方案

使用C#生成dll文件并调用(2010-06-05 16:19:16)转载▼标签&#xff1a;杂谈使用C#生成dll文件并调用2009-11-21 09:00http://hi.baidu.com/wuxiaoming1733/blog/item/8e6b8c020ea23f074bfb51d5.html一、创建dll文件&#xff1a;例如生成一个md5编码判断状态的文件&#xff0c;即…...

电商网站开发工作计划/手机最新产品新闻

我在Java私塾学习期间的学习源码好的话就顶一下。。。下载地址&#xff1a;http://sishuok.com/forum/posts/list/5164.html我把我在java私塾学习的源码分享给大家&#xff0c;希望对大家有用&#xff0c;请使用非360和腾讯tt浏览器下载&#xff0c;源码包括&#xff1a;1、我的…...