【线程】封装 | 安全 | 互斥

这篇具有很好参考价值的文章主要介绍了【线程】封装 | 安全 | 互斥。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

线程封装(面向对象)

1.组件式的封装出一个线程类(像C++11线程库那样去管理线程)

  • 我们并不想暴露出线程创建,终止,等待,分离,获取线程id等POSIX线程库的接口,我们也想像C++11那样通过面向对象的方式来玩,所以接下来我们将POSIX线程库的接口做一下封装,同样能实现像C++11线程库那样去管理我们的线程,这个类就像一个小组件似的,包含对应的.hpp文件就可以使用,使用起来很舒服。

【线程】封装 | 安全 | 互斥,Linux,linux,C++

//hpp文件
#pragma once
#include<iostream>
#include<pthread.h>
#include<string>
#include<functional>

using namespace std;

template<class T>
using fun_c = std::function<void(T)>;

template<class T>
class Thread
{
public:
    Thread(const string& threadname, fun_c<T> func, T data)
    :_threadname(threadname), _func(func), _data(data)
    {} 

    ~Thread()
    {
        ;
    }

    //线程执行的函数
    static void* ThreadRoutine(void* args)  //成员函数默认里面有一个this指针,而静态成员函数中没有
    {
        Thread* t = static_cast<Thread*>(args);
        t->_func(t->_data);
        return nullptr;
    }

    //线程创建-》running
    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this/*?*/);
        if(n == 0) 
        {
            _isrunning = true;
            return true;
        }
        else return false;
    }

    bool Join()
    {
        if(!_isrunning) return true;
        int n = pthread_join(_tid, nullptr);
        if(n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }

    std::string ThreadName()
    {
        return _threadname;
    }

    bool IsRunning()
    {
        return _isrunning;
    }

private:
    string _threadname;
    pthread_t _tid = 0;
    bool _isrunning = false;
    fun_c<T> _func;
    T _data;
};

【线程】封装 | 安全 | 互斥,Linux,linux,C++

//.cc文件
#include <iostream>
#include "mythread.hpp"
#include <cstdio>
#include<unistd.h>

string GetThreadName()
{
    static int num = 1;
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "thread-%d", num++);
    return buffer;
}

void Print(int number)
{
    while (number)
    {
        std::cout << "hello world: " << number-- <<  std::endl;
        sleep(1);
    }
}

int main()
{
    Thread<int> t(GetThreadName(), Print, 10);
    std::cout << "is thread running? " << t.IsRunning() << std::endl;
    t.Start(); 
    std::cout << "is thread running? " << t.IsRunning() << std::endl;
    t.Join();  //线程阻塞等待
    // Thread t(GetThreadName(), Print);
    // std::cout << "is thread running? " << t.IsRunning() << std::endl;

    // t.Start();

    // std::cout << "is thread running? " << t.IsRunning() << std::endl;

    // t.Join();  //线程阻塞等待
    return 0;
}

线程互斥

多线程访问共享资源是不安全的

假设现在有一份共享资源tickets,如果我们想让多个线程都对这个资源进行操作,也就是tickets- -的操作,但下面两份代码分别出现了不同的结果,上面代码并没有出现问题,而下面代码却出现了票为负数的情况,这是怎么回事呢?
其实问题产生就是由于多线程被调度器调度的特性导致的。

int ticket = 1000;  //票数
void route(string arg)
{
    while (1)
    {
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", arg.c_str(), ticket);
            ticket--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    Thread<string> t(GetThreadName(), route, GetThreadName());
    Thread<string> t2(GetThreadName(), route, GetThreadName());
    Thread<string> t3(GetThreadName(), route, GetThreadName());
    Thread<string> t4(GetThreadName(), route, GetThreadName());
    
    
    t.Start(); 
    t2.Start(); 
    t3.Start(); 
    t4.Start(); 

    t.Join(); 
    t2.Join(); 
    t3.Join(); 
    t4.Join(); 
    return 0;
}

【线程】封装 | 安全 | 互斥,Linux,linux,C++

  • 了解上面的问题需要知道线程调度的特性,实际线程在被调度时它的上下文会被加载到CPU的寄存器中,而线程在被切换的时候,线程又会带着自己的上下文被切换下去,此时要进行线程的上下文保存,以便于下次该线程被切换上来的时候能够进行上下文数据的恢复。
    除此之外,像tickets- -这样的操作,对应的汇编指令其实至少有三条:1.读取数据 2.修改数据 3.写回数据,而线程函数我们知道会在每个线程的私有栈都存在一份,在上面的例子中多个线程执行同一份线程函数,所以这个线程函数就绝对会处于被重入的状态,也就绝对会被多个线程执行!今天我们假设只有一个CPU(CPU就是核心,处理器芯片会集成多个核心)在调度当前进程中的线程,那么线程是CPU调度的基本单位,所以也就会出现一个线程可能执行一半的时候被切换下去了,并且该线程的上下文被保存起来(线程在切换的时候,会将自己的上下文数据保存到线程的线程控制块TCB),然后CPU又去调度进程中的另一个线程。

【线程】封装 | 安全 | 互斥,Linux,linux,C++

在知道上面的原理之后,还需要知道usleep的作用,当usleep放到if分支语句的第一行时,票数就出现了问题,出现了负数,主要是因为usleep可以将线程暂时阻塞,那么CPU就会把他切换下去,转而执行其他线程,但需要注意的是,如果被切换的线程重新调度上来时,还会从上次他执行后的语句继续向下运行。
所以会出现多个线程同时进入到分支判断语句,然后去阻塞等待的情况,假设tickets已经变成了1,然后其余的线程此时都被调度上来了,他们都开始执行tickets- -,- -之后不满足循环条件线程才会退出,那么如果我们创建出了4个线程,就会有3个线程在票数已经为0的情况下继续减减,所以就会出现票数为负数的情况。

提出解决方案:加锁(局部和静态锁的两种初始化/销毁方案)

那该如何解决上面的问题呢?多个执行流操作共享资源时,发生了数据不一致问题。
解决上面的问题实际要通过加锁来实现,但在谈论加锁的话题之前,我们需要来重新看待几个概念。

  • 多个执行流总是能够共享许多资源,共享资源我们称为临界资源。
  • 而多个执行流执行的函数体内部,对临界资源进行操作的代码称为临界区,需要注意的是临界区不是整个函数体内部的代码,而是指对共享资源进行操作的代码称为临界区。
  • 如果我们想让多个执行流串行的访问临界资源,而不是并发或并行的访问临界资源,这样的线程调度方案就是互斥式的访问临界资源!(串行就是指只要一个线程开始执行这个任务,那么他就不能中断,必须得等这个线程执行完这个任务,你才能切换其他线程执行其他的任务,这个概念等会讲完锁之后大家就明白什么是互斥了)
  • 当线程在执行一个对资源访问的操作时,要么做了这个操作,要么没有做这个操作,只要两种状态,不会出现做了一半这样的状态,我们称这样的操作是原子性的
    【线程】封装 | 安全 | 互斥,Linux,linux,C++

有了上面四组概念的铺垫之后,我们来谈谈如何对共享资源进行加锁和解锁?
首先锁实际就是一种数据类型,这个锁就像我们平常定义出来的变量或是对象一样,只不过这个锁的类型是系统给我们封装好的一种类型,进行重定义后为pthread_mutex_t。
【线程】封装 | 安全 | 互斥,Linux,linux,C++
变量或对象在定义的时候也是可以初始化的,变量初始化后,就是变量的定义,而不是声明了。变量和对象也都有自己的销毁方案,内置类型的变量或者对象销毁时,操作系统会自动回收其资源或者自动调用析构函数,而自定义对象销毁时,操作系统会调用其析构函数进行资源的回收。

锁同样也是如此,锁也有自己的初始化和销毁方案,如果你定义的是一把局部锁,就需要用pthread_mutex_init()和pthread_mutex_destroy()来进行初始化和销毁,如果你定义的是一把全局锁或静态所,则不需要用init初始化和destroy销毁,直接PTHREAD_MUTEX_INITIALIZER进行初始化即可,它有自己的初始化和销毁方案,我们无须关心静态或全局锁如何销毁。【线程】封装 | 安全 | 互斥,Linux,linux,C++

定义好锁之后,我们就可以对某一段代码进行加锁和解锁,加锁与解锁意味着,这段代码不是一般的代码,只有申请到锁,持有锁的线程才能访问这段代码,加锁和解锁之间的代码可以称为临界区,因为想要访问这段空间必须有锁才可以访问。pthread_mutex_lock实际就是申请锁的代码和临界区的入口,如果你申请锁成功了,那么你就可以进入临界区访问临界资源,如果你并没有申请成功,比如当前这把锁已经被别的线程申请到并持有了,其他线程正持有锁在临界区访问着呢,那么你就无法进入临界区,因为你并没有持有锁,必须得在pthread_mutex_lock阻塞等待,直到你申请到锁之后,你才能进入临界区访问临界资源,这样的线程访问实际就是互斥,指的是当一个线程正在持有锁访问临界区的时候,其他线程无法进入临界区,直到持有锁的线程释放锁之后才会有可能进入临界区,注意是有可能,因为当线程释放锁之后,这把锁还需要被竞争,哪个线程竞争到这把锁,哪个线程才能持有锁的访问临界资源!【线程】封装 | 安全 | 互斥,Linux,linux,C++

【线程】封装 | 安全 | 互斥,Linux,linux,C++

上面谈论完锁的初始化和销毁,以及如何加锁和解锁之后,我们来利用锁解决上面出现的共享资源访问不安全的问题。你不是由于多线程再进行临界资源访问时,可能由于线程切换什么的,导致非原子性式的访问临界资源吗?那我不让你这么干,我对这段临界资源进行加锁,让你当前申请到锁正在访问临界资源的线程,必须给我以原子性的访问来访问临界资源,换句话说,你必须把访问临界资源的工作做完了,才可以,要么你不要访问临界资源,要么你访问了临界资源,就必须把临界资源全部访问完了,中间不能访问一半就不访问了!所以只要对临界资源进行加锁后,临界资源就变得安全了,因为无论什么线程想要访问临界资源,都必须以原子性的方式访问完,这样的话,就不会出现在访问一半的时候,线程被切换下去了,其他线程被切换上来继续访问临界资源了,而是说如果持有锁的线程被切换下去了,这个线程会抱着他申请到的锁被切换下去,此时其他线程如果被切换上来,想要访问临界资源,那也没用,因为你没有锁啊!持有锁的线程被切换时,是抱着锁被切换的,那你现在既然访问不了临界区,CPU无法继续执行代码,那就只能等持有锁的线程重新被切换上来时,才能继续开展临界资源的访问工作,这个工作必须且只能由申请到锁的线程来完成,其他任何线程都无法完成这个工作!反过来说,这不就是原子性吗?访问临界资源的工作只要被持有锁的线程开始做了,哪怕他在做的过程中被切换下去了,也无须担心,因为别的线程做不了这个工作,所以还是得等持有锁的线程被切换上来的时候才能继续做这个工作,那是不是这个工作只要开始做了,就一定会被做完呢?会不会出现做一半,停下来了不做了,让别的线程在去访问临界资源的情况呢?当然不会!这就是锁带来的作用。

【线程】封装 | 安全 | 互斥,Linux,linux,C++
如果在加锁之后运行代码,实际可以发现他抢票的速度是要比没加锁之前慢的,原因也很简单。我来给大家解释一下,没加锁之前,线程之间是可以并发或并行执行的,我先大概说一下并发和并行是什么,后面会详细介绍这两者的区别和概念,并发你可以简单理解为,当线程运行一半被切换下去的时候,此时CPU还可以调度运行其他线程,也就是说,如果多个线程在运行的时候,每个线程都会被CPU跑一跑,那在一段时间内,所有的线程都可以被执行到,并且推进每个线程的执行过程。而并行就是在多个核心上面同一时刻跑不同的线程,比如两个同时访问临界资源的线程,在未加锁的时候,可能出现多个核心同时执行两个线程的代码,同时在访问临界资源,但实际这种情况并不常见,因为我们写出来的代码优先级并没有那么高,所以基本上都是在按照并发执行的。
然后加锁前是并发执行的,也就是说在一个线程被切换下去的时候,其他- -tickets的线程还能够被重新调度上来进行票数的- -,那么总体上来说,票数就会被一直- -。
而加锁之后就不是并发执行的了,因为我们上面说过,加锁之后即使持有锁的线程被切换下去,其他被调度到CPU上的线程也是无法进行票数- -的,因为他们没有锁,所以在持有锁的线程被切换下去的这段时间里,票数不会改变,因为线程在串行的访问临界资源,什么是串行呢?就是一个线程访问完之后,才能轮到另一个线程,就是我们前面说的,一个线程在完成他的工作之后,释放完锁之后,其他线程才有可能竞争到锁,才有可能访问临界资源,这样就是串行。
串行的执行效率肯定要比并发执行的效率底嘛,因为当多线程在执行任务的时候,我们进行并发执行,为的就是当前线程如果被切换下去了,那也没啥事,因为其他被调度上来的线程依旧可以执行这个任务。你现在加锁之后就会变成串行执行了,那当前持有锁的线程被切换下去时,其他被调度上来的线程是无法继续执行任务的,效率自然就会底一些。(效率底一点就底一点吧,毕竟现在共享资源就安全了嘛,下面运行结果你也可以看到,没有锁的时候,票数就为负数了,这种情况用户怎么可能容忍。)
【线程】封装 | 安全 | 互斥,Linux,linux,C++

局部和全局锁的两种加锁方案的代码实现

如果定义局部锁的话,我们肯定是想要将这把锁传给每个线程的,让每个线程都用这把锁来互斥式的访问共享资源,以此来保证共享资源的安全性。

//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  //初始化一把全局的锁
int ticket = 1000;  //票数
void route(pthread_mutex_t* mutex)
{
    while (1)
    {
        pthread_mutex_lock(mutex);  //上锁
        if (ticket > 0)
        {
            usleep(1000);
            printf("sells ticket:%d\n", ticket);
            ticket--;
            pthread_mutex_unlock(mutex);

        }
        else
        {
           pthread_mutex_unlock(mutex);
            break;
        }
    }
}

int main()
{
    pthread_mutex_t mutex;  
    pthread_mutex_init(&mutex, nullptr); //初始化一把局部的锁

    Thread<pthread_mutex_t*> t(GetThreadName(), route, &mutex);
    Thread<pthread_mutex_t*> t2(GetThreadName(), route, &mutex);
    Thread<pthread_mutex_t*> t3(GetThreadName(), route, &mutex);
    Thread<pthread_mutex_t*> t4(GetThreadName(), route, &mutex);
    
    t.Start(); 
    t2.Start(); 
    t3.Start(); 
    t4.Start(); 

    t.Join(); 
    t2.Join(); 
    t3.Join(); 
    t4.Join(); 
    pthread_mutex_destroy(&mutex); //释放锁
    return 0;
}

全局的锁定义并且初始化以后,每个线程都可以看得见,因此也是共享资源

如何看待锁

完成上面对于共享资源访问不安全问题的解决之后,我们来深入的理解一下锁。
我们知道,共享资源在被多线程访问时,是不安全的,所以我们需要加锁来保护共享资源。但是我们回过头来想一想,锁本身是不是共享资源呢?所有的线程都需要申请锁和释放锁,那不就是在共同的访问锁这个资源嘛?所以锁本身不就是共享资源吗?那多个线程在访问锁这个共享资源的时候,锁本身是不是需要被保护呢?当然需要!其他的共享资源可以通过加锁来进行保护,那锁怎么办呢?
实际上,加锁和解锁的过程是原子的!也就是说只要你申请了锁,并且竞争能力恰好足够,那么你就一定能够拿到这个锁,否则你就不会拿到这个锁,不会说在申请锁申请一半的时候,线程被切换下去了,其他线程去申请锁了,不会出现这种中间态的情况!既然加锁和解锁的过程是原子的,那其实访问锁就是安全的!(但加锁解锁的过程为什么是原子的呢?我该如何理解呢?这个后面会说。)
【线程】封装 | 安全 | 互斥,Linux,linux,C++
【线程】封装 | 安全 | 互斥,Linux,linux,C++

RAII风格的封装设计锁?(构造函数加锁,析构函数解锁)

如果我们想简单的封装使用锁,那我们该如何设计呢?我们也想像之前封装设计线程那样搞出来C++式的面向对象版的创建线程和销毁线程。
实际实现起来也很简单,无非就是对原生的申请锁,加锁,解锁接口的封装!我们先定义一个互斥量的类,类中实现构造函数将锁的地址进行初始化,然后定义出加锁和解锁的两个接口,这样就可以定义出来一个内部能够进行加锁和解锁的类。
然后我们再加一层封装,实现出RAII( Resource Acquisition Is Initialization)风格的加锁,即为构造函数处进行加锁,析构函数处进行解锁!
至于锁的初始化和销毁方案,是类外面的事情,使用时需要自己先初始化好一把锁,确定初始化和销毁的方案,然后利用LockGuard.hpp这个小组件来进行加锁和解锁的过程!

【线程】封装 | 安全 | 互斥,Linux,linux,C++
【线程】封装 | 安全 | 互斥,Linux,linux,C++

可重入函数和线程安全

在多线程并发执行代码,同时访问共享资源的时候,如果某一个共享资源由于多线程访问,发生了数据不一致,共享资源不安全,并且导致其他线程运行出问题了,那么这种情况就是线程不安全的。尤其对于没有锁保护的共享资源的多线程访问的代码,很大概率出现线程不安全的情况。
而什么是可重入呢?这个话题并不陌生,我们之前谈论进程信号的时候,进程可能由于收到信号,并且在陷入内核时检测到信号,跳转到handler方法执行信号处理函数,信号处理函数中可能会出现和main执行流中执行相同的函数体,例如当时我们所说的链表的push_back在main和handler中同时执行,可能会导致某些未知错误的产生,如果出现了问题,那么我们称这个函数是不可重入函数,如果没有出现问题这个函数就是可重入函数。值得注意的是,不可重入函数说的是这个函数的属性,而不是说这个函数叫做不可重入函数,那么他就一定不能被执行流所重入,只是说,他如果被执行流重入,极大概率是要出问题的。
【线程】封装 | 安全 | 互斥,Linux,linux,C++

【线程】封装 | 安全 | 互斥,Linux,linux,C++

死锁

死锁是指一个进程中的各个线程,都持有着锁,但同时又去申请其他线程的锁,而每个线程持有的锁都是占有不会释放的,所以大家都会等着,等对方先释放锁,但是呢,大家又都不释放锁,全都占有着锁,所以大家就会处于一种永久等待的状态,也就是永久性的阻塞状态,所有执行流都不会被运行,这样的问题就是死锁!
之前抢票的代码中,多个线程使用的是同一把锁,未来有些场景一定是要使用多把锁的,在多把锁的情况下,如果某些线程持有锁不释放,还要去申请其他线程正持有的锁,而每个线程都是这样的状态,那就是死锁问题。

产生死锁的四个必要条件
1.互斥条件:一个资源每次只能被一个执行流使用,互斥其实就是加锁之后线程的串行执行。
2.请求与保持条件:一个执行流由于请求资源而阻塞时,对自己已经获得的资源保持不放。说白了就是我自己的东西不释放,我还要你的东西,你不给我就一直等,等到你给我为止。
3.不剥夺条件:一个线程在未使用完自己获得的资源之前,是不能够强行剥夺其他线程的资源的。说白了就是你先在还有资源呢,你想要别人的自由你就得等,不能强行剥夺!当你使用完自己的资源后,你可以去等待申请别人的资源。总之就是不能强行剥夺其他线程的资源,想要就必须阻塞等待别人释放资源才可以。
4.循环等待条件:若干个执行流之间,形成一种头尾相接的互相等待对方资源的关系。我们也称这样的现象为环路等待。文章来源地址https://www.toymoban.com/news/detail-842339.html


到了这里,关于【线程】封装 | 安全 | 互斥的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 『Linux』第九讲:Linux多线程详解(三)_ 线程互斥 | 线程同步

    「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(二),今天这篇是 Linux多线程详解(三),内容大致是线程互斥与线程同步,讲解下面开始! 「归属专栏」Linux系统编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「每篇一句

    2024年02月02日
    浏览(74)
  • Linux——线程3|线程互斥和同步

    我们上一篇提到过,多个线程执行下面代码可能会出错,具体原因可查看上一篇Linux博客。 为避免这种错误的出现,我们可采用加锁保护。 PTHREAD_MUTEX_INITIALIZER 用pthread_mutex_t定义一把锁。ptherad_mutex_init是对锁进行初始化的函数。如果这把锁是全局的并且是静态定义的,我们可

    2024年02月05日
    浏览(48)
  • 【Linux】线程同步和互斥

    1.临界资源:多线程执行流共享的资源,且一次只能允许一个执行流访问的资源就叫做临界资源。(多线程、多进程打印数据) 2.临界区:每个线程内部,访问临界资源的代码,就叫做临界区。 3.互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对

    2024年02月08日
    浏览(46)
  • Linux之线程互斥

    目录 一、问题引入 二、线程互斥 1、相关概念 2、加锁保护 1、静态分配 2、动态分配 3、锁的原理 4、死锁 三、可重入与线程安全 1、概念 2、常见的线程不安全的情况 3、常见的线程安全的情况 4、常见不可重入的情况 5、常见可重入的情况 6、可重入与线程安全联系 7、可重

    2024年03月16日
    浏览(91)
  • Linux->线程互斥

    前言: 1 线程互斥 1.1 多线程并发问题 1.2 线程锁 1.3 锁的接口 2 线程安全与可重入函数  3 死锁         本篇文章主要讲解了线程互斥的实现方式,还有原理,并附上代码讲解。并且讲解了锁的概念,问题等。         还记得我上一篇文章的结尾有提过的问题吗?如果多个

    2024年02月10日
    浏览(40)
  • Linux多线程互斥锁

    迷途小书童的 Note 读完需要 5 分钟 速读仅需 2 分钟 1 引言 在 Linux 编程中,多线程是一种常见的并发编程模型。为了保证多线程之间的数据同步和互斥访问,pthread_mutex(互斥锁)是一个重要的工具。本文将深入探讨 pthread_mutex 的底层实现原理、函数原型,并提供详细的使用方

    2024年02月10日
    浏览(54)
  • linux:线程互斥

    个人主页 : 个人主页 个人专栏 : 《数据结构》 《C语言》《C++》《Linux》 本文是对于线程互斥的知识总结 我们先看下面代码。 该代码创建三个线程-1,线程-2,线程-3,去抢夺ticket资源,当ticket从10000依次减到0时,三个线程退出。那该代码运行结果是什么呢?ticket最后会是

    2024年03月21日
    浏览(40)
  • 【linux】线程互斥

    喜欢的点赞,收藏,关注一下把! 到目前为止我们学了线程概念,线程控制接下来我们进行下一个话题,线程互斥。 有没有考虑过这样的一个问题,既然 线程一旦被创建,几乎所有资源都是被所有线程共享的。 那多个线程访问同一份共享资源有没有什么问题? 下面我们模

    2024年02月03日
    浏览(40)
  • Linux--线程--互斥锁

    1.互斥量 a)互斥量(mutex)从本质上来说是一把锁,一般在主线程中定义一个互斥量,就是定义一把锁。然后根据我们的需求来对线程操作这把锁。 b)如果给所有的线程都加上锁了,线程们会去争取内存空间,谁先争取到谁先运行,直到该线程解锁后,期间其他线程只能等

    2024年02月06日
    浏览(43)
  • 【关于Linux中----线程互斥】

    先来用代码模拟一个抢票的场景,四个线程不停地抢票,一共有1000张票,抢完为止,代码如下: 执行结果如下: 可以看到,最后出现了票数为负数的情况,很显然这是错误的,是不应该出现的。 为什么会出现这种情况? 首先要明确,上述的几个线程是不能同时执行抢票的

    2023年04月19日
    浏览(73)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包