信号量概念,使用场景,本质,接口函数(pv操作),基于环形队列的生产消费者模型(过程,三个原则,单线程,多线程)

目录

引入​​​​​​​

介绍

概念 

使用场景

引入

介绍

注意

本质

计数器的本质

[判断资源是否就绪]

和互斥锁的关联

接口函数

初始化和销毁信号量

sem_init

函数原型

sem 

pshared

value 

sem_destroy

pv操作

sem_wait

​编辑

sem_post

其他接口

sem_getvalue

基于环形队列的生产消费者模型

引入

介绍

生产消费过程

三个原则

解释

并发性 

单生产单消费

思路

代码

ring_queue.hpp

task.hpp

main.cpp

示例

​编辑

多生产多消费

思路

代码

ring_queue.hpp

main.cpp 

示例


引入​​​​​​​

在之前我们就已经简单介绍了信号量这一概念和用途:

临界资源,临界区,通信的干扰问题(互斥),信号量(本质,上下文切换问题,原子性,自身的安全性,操作)_临界区内不允许发生上下文切换-CSDN博客

本篇文章将继续讨论信号量,让我们来更好地了解和使用它

介绍

概念 

  • 信号量是一种用于多线程或多进程同步和互斥的机制,用于协调并发执行的程序, 确保在任何给定时刻只有一个线程或进程能够访问共享资源,从而避免竞态条件和数据损坏
  • 信号量是一种计数器,用于控制对共享资源的访问

使用场景

引入

  • 前面我们已经介绍了互斥锁,他可以用于保证互斥访问临界资源
  • 这里的信号量也有同样的作用

既然已经有了互斥锁,又为什么会有信号量

  • 那自然是有它的独特用处
  • 互斥锁适用于保证只有一个线程进入一份资源
  • 如果该资源可以被看作多份,若仍然只有一个线程进入,未免效率太低
  • 因为可以被看作多份的资源,可以让同样数量的线程并发访问
  • 所以,为了更高的效率,便提出了信号量这一概念,来保证在该情况下并发的安全性

介绍

  • 当一份共享资源被划分成n份时,需要信号量来保证只有n个线程并发
  • 否则就会出现多个线程同时进入一份临界资源,也就会出现一系列问题(比如数据紊乱等等)

注意

  • 信号量只是保证了进入资源的线程数是合理的
  • 并没有保证这些线程的进入不冲突 (这些是我们的工作捏,由代码来合理分配线程)

本质

信号量本质就是原子性的计数器

  • 该计数器有p操作(也就是--)
  • v操作(也就是++)

计数器的本质

计数器用于描述资源的数量

[判断资源是否就绪]

信号量将判断资源是否就绪这一操作,放在了访问临界资源之外,也就是放在了自己的操作中

  • 之前写的用互斥锁保证互斥性的cp代码中,我们是加锁区域中判断资源是否就绪
  • 也就是先拿到锁,才能去访问临界资源,判断其是否就绪
  • 但这里不同,只要拿到了信号量,其中一份资源就已经分配给你了,也就不需要判断就绪了

  • 因为信号量的核心操作就是pv
  • 只有当资源就绪时,才能完成p操作,而完成了p操作,就申请到了信号量
  • 反过来论证, 申请到信号量,就代表此时资源就绪

  • 所以,我们是先确定了资源就绪,才去访问临界资源的

和互斥锁的关联

我们带着信号量的概念回过头去看之前写的cp代码:

  • 当时是将资源看成一个整体的,那么信号量就是1
  • 所以,只会有一个线程进入资源 == 信号量要么为0,要么为1

这是否也可以对应申请锁和释放锁的过程呢?

  • 申请到锁=信号量变为0=资源就绪
  • 释放锁=信号量变为1
  • 所以,我们可以将互斥锁看作初始值为1的信号量

接口函数

POSIX 信号量相关接口在<semaphore.h>头文件中

初始化和销毁信号量

sem_init

函数原型

sem 
  • 其类型为sem_t
  • 和之前pthread库中用于标识互斥锁的mutex参数一样(类型为pthread_mutex_t),都是标识符
pshared
  • 指定信号量的类型
  • 0表示线程间共享,非零表示进程间共享
value 

信号量的初始值

sem_destroy

用于销毁信号量,并释放相关的资源

pv操作

sem_wait

对信号量执行p操作(也就是等待信号响应)

  • 如果信号量的值大于0,p操作成功(也就是成功获取相关资源,且信号量计数-1)
  • 如果信号量的值为0,线程将被阻塞,等待资源的释放(也就是信号量的计数增加)

sem_post

对信号量执行v操作(也就是发送信号)

  • 信号量计数+1
  • 如果有等待的线程,唤醒其中一个(或多个)线程

其他接口

sem_getvalue

获取信号量的当前值,将其存储到sval变量中

基于环形队列的生产消费者模型

引入

之前我们已经学习过基于阻塞队列的生产消费者模型:

而下面要介绍的,是基于环形队列的cp模型(大差不差嘟,只要了解他生产消费的过程,就不难写代码)

介绍

  • 环形队列是一种数据结构,常用于实现缓冲区或队列的循环存储
  • 在环形队列中,队列的尾部与头部相连,形成一个环状结构
  • 当队列满时,新的元素会覆盖队列头部的元素,实现了循环利用的目的
  • --图源网络
  • 和之前学习过的环形队列的操作类似,但这里被赋予了生产者和消费者的含义
  • 因此我们来重新梳理一下push和pop的操作

生产消费过程

我们将头指针看作消费者,尾指针看作生产者(因为消费的基础是生产,必然是生产者先跑, 消费者追着生产者走)

  • 生产者当前所指位置,是即将生产资源的位置
  • 消费者当前所指位置,是即将消费资源的位置
  • 也就是说,两者都是先在当前位置完成任务,再移动位置

起始时,生产者和消费者指向同一位置(标识此时无资源):

如果当前有资源且不满时,生产者和消费者必然在不同位置:

如果资源满了,两者将会再次指向同一位置:

三个原则

根据这个环形队列的实际意义,我们可以总结出三个原则:

  • 消费者不可以超过生产者
  • 生产中不可以领先消费者一圈
  • 二者在同一位置时,必须互斥

解释

前两条很容易理解 -- 消费的前提是已经有资源,资源满了自然无法生产

而第三条:

  • 从前面的图中我们可以知道,当两者在同一位置时,也就意味着资源要么满,要么空
  • 满时只有消费者能动,空时只有生产者能动
  • 所以要在这两种情况下,让二者互斥(也就是保证只有一方能动)

并发性 

注意,因为只有当在同一位置时,才需要互斥,也就是说 -- 不在同一位置的话,两者就可以并发进行了

单生产单消费

思路

实际和之前写的基于阻塞队列的代码类似

生产消费者模型(引入--超市),321原则,阻塞队列实现+优点(代码,伪唤醒问题,条件变量接口wait中锁的作用),进阶版实现(生产任务,RAII风格),多生产多消费实现+优点-CSDN博客

  • 定义一个环形队列模型的类,里面封装出需要的操作(生产和消费)

我们从之前的介绍中,可以知道需要的变量:

  • 一个环形队列(用数组模拟)
  • 定义队列大小
  • 两个当前位置(生产者和消费者)
  • 两个信号量

信号量我们着重说一下:

  • 信号量实际是计数器,所以是用于定义有数量的东西
  • 生产需要空间资源,所以它看中的是目前还剩多少空间
  • 消费需要数据资源,所以它看中的是目前有多少数据
  • 空间数量+数据数量=队列容量
  • 所以,一旦其中一方达到极大值(也就是容量),另一方就是0
  • 那么,另一方必然就无法完成p操作,自然也就形成互斥
  • 所以,我们就可以定义相应的两个信号量,两者互相制约,上面总结的三个原则自然就被保证了

代码

ring_queue.hpp
#include <pthread.h>
#include <vector>
#include <stdlib.h>
#include <semaphore.h>

using namespace std;

static const int def = 5;
template <class T>
class ring_queue
{
private:
    void P(sem_t *sem)
    {
        sem_wait(sem);
    }
    void V(sem_t *sem)
    {
        sem_post(sem);
    }

public:
    ring_queue(int num = def)
        : c_i(0), p_i(0), capacity_(num), rq_(num)
    {
        sem_init(&data_num_, 0, 0);
        sem_init(&cap_num_, 0, num);
    }
    ~ring_queue()
    {
        sem_destroy(&data_num_);
        sem_destroy(&cap_num_);
    }
    void push(const T data)
    {
        P(&cap_num_);
        rq_[p_i] = data;
        p_i = (p_i + 1) % capacity_;
        V(&data_num_);
    }
    void pop(T &data)
    {
        P(&data_num_);
        data = rq_[c_i];
        c_i = (c_i + 1) % capacity_;
        V(&cap_num_);
    }

private:
    vector<T> rq_;
    int c_i;
    int p_i;
    int capacity_;

    sem_t data_num_;
    sem_t cap_num_;
};

除此之外,我们还需要构建出任务:

task.hpp
#pragma once
// 生成二元整数运算任务(加减乘除),有错误码提示
// 1为/0操作,2为%0操作,3为非法错误

#include <iostream>
#include <string>

using namespace std;

string symbol = "+-*/%";

class Task
{
public:
    Task() {} // 方便只是为了接收传参而定义一个对象
    Task(int x, int y, char c)
        : x_(x), y_(y), code_(0), op_(c), res_(0)
    {
    }
    int get_result()
    {
        return res_;
    }
    int get_code()
    {
        return code_;
    }
    string get_task()
    {
        string task = to_string(x_) + op_ + to_string(y_) + " = ?";
        return task;
    }
    void operator()()
    {
        switch (op_)
        {
        case '+':
            res_ = x_ + y_;
            break;
        case '-':
            res_ = x_ - y_;
            break;
        case '*':
            res_ = x_ * y_;
            break;
        case '/':
            if (y_ == 0)
            {
                code_ = 1;
                break;
            }
            res_ = x_ / y_;
            break;
        case '%':
            if (y_ == 0)
            {
                code_ = 2;
                break;
            }
            res_ = x_ % y_;
            break;
        default:
            code_ = 3;
            break;
        }
    }
private:
    int x_;
    int y_;
    int res_;
    int code_;
    char op_;
};
main.cpp

主线程的任务就是构建出两个线程,分别去生产和消费

注意,不要忘记如何获取任务,以及获取到任务需要处理它哟~

#include "ring_queue.hpp"
#include "Task.hpp"
#include <random>
#include <time.h>
#include <unistd.h>

int sym_size = symbol.size();

void *consume(void *args)
{
    ring_queue<Task> *rq = static_cast<ring_queue<Task> *>(args);
    while (true)
    {
        Task t;
        //消费
        rq->pop(t);
        //处理任务
        t();
        cout << "im consumer,task is " << t.get_task() << " ,result is " << t.get_result() << " ,code is " << t.get_code() << endl;
    }
}
void *product(void *args)
{
    ring_queue<Task> *rq = static_cast<ring_queue<Task> *>(args);
    while (true)
    {
        //获取任务
        int x = rand() % 10 + 1;
        int y = rand() % 5;
        char op = symbol[rand() % (sym_size - 1)];
        Task t(x, y, op);
        //生产
        rq->push(t);
        cout << "im productor,task is " << t.get_task() << endl;
        sleep(1);
    }
}
int main()
{
    srand(time(nullptr) ^ getpid());
    ring_queue<Task> rq(5);
    pthread_t c, p;
    pthread_create(&c, nullptr, consume, &rq);
    pthread_create(&p, nullptr, product, &rq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    return 0;
}

示例

如果生产比较慢,消费线程就会等待生产线程的生产

  • 和我们预想的一样,生产一个任务,随后就消费一个任务:

如果消费比较慢,生产线程就会立即生产任务直至上限

  • 而消费者会慢慢一个一个消费
  • 消费一个就挪出一个空位,生产线程就会生产一个任务:

多生产多消费

思路

和单线程不同的是,我们需要保证生产者/消费者之间的互斥性(我们不能让多个生产者对同一位置进行生产,消费也同理)

所以我们需要加锁(生产者和消费者各自有一把锁)

代码

ring_queue.hpp

增加了解锁加锁操作

#include <pthread.h>
#include <vector>
#include <stdlib.h>
#include <semaphore.h>

using namespace std;

static const int def = 5;
template <class T>
class ring_queue
{
private:
    void P(sem_t *sem)
    {
        sem_wait(sem);
    }
    void V(sem_t *sem)
    {
        sem_post(sem);
    }
    void lock(pthread_mutex_t *mutex)
    {
        pthread_mutex_lock(mutex);
    }
    void unlock(pthread_mutex_t *mutex)
    {
        pthread_mutex_unlock(mutex);
    }

public:
    ring_queue(int num = def)
        : c_i(0), p_i(0), capacity_(num), rq_(num)
    {
        sem_init(&data_num_, 0, 0);
        sem_init(&cap_num_, 0, num);
        pthread_mutex_init(&c_mutex_, nullptr);
        pthread_mutex_init(&p_mutex_, nullptr);
    }
    ~ring_queue()
    {
        sem_destroy(&data_num_);
        sem_destroy(&cap_num_);
        pthread_mutex_destroy(&c_mutex_);
        pthread_mutex_destroy(&p_mutex_);
    }
    void push(const T data)
    {
        P(&cap_num_);

        lock(&p_mutex_);
        rq_[p_i] = data;
        p_i = (p_i + 1) % capacity_;
        unlock(&p_mutex_);

        V(&data_num_);
    }
    void pop(T &data)
    {
        P(&data_num_);

        lock(&c_mutex_);
        data = rq_[c_i];
        c_i = (c_i + 1) % capacity_;
        unlock(&c_mutex_);

        V(&cap_num_);
    }

private:
    vector<T> rq_;
    int c_i;
    int p_i;
    int capacity_;

    sem_t data_num_;
    sem_t cap_num_;

    pthread_mutex_t c_mutex_;
    pthread_mutex_t p_mutex_;
};
main.cpp 

多线程的结果比较不清晰,所以在代码中添加了不少sleep

以及,把多线程和单线程的代码分开了:

#include "ring_queue.hpp"
#include "Task.hpp"
#include <random>
#include <time.h>
#include <unistd.h>

int sym_size = symbol.size();

struct thread
{
    thread(ring_queue<Task> *rq, string name)
        : rq_(rq), name_(name)
    {
    }
    ring_queue<Task> *rq_;
    string name_;
};

void *consume_single(void *args)
{
    ring_queue<Task> *rq = static_cast<ring_queue<Task> *>(args);
    while (true)
    {
        usleep(20);
        Task t;
        rq->pop(t);
        // 处理任务
        t();
        cout << "im consumer,task is " << t.get_task() << " ,result is " << t.get_result() << " ,code is " << t.get_code() << endl;
        // sleep(1);
    }
}
void *product_single(void *args)
{
    ring_queue<Task> *rq = static_cast<ring_queue<Task> *>(args);
    while (true)
    {
        // 生产任务
        int x = rand() % 10 + 1;
        int y = rand() % 5;
        char op = symbol[rand() % (sym_size - 1)];
        Task t(x, y, op);

        rq->push(t);
        cout << "im producer,task is " << t.get_task() << endl;
        sleep(1);
    }
}

void *consume_multiple(void *args)
{
    thread *t = static_cast<thread *>(args);
    ring_queue<Task> *rq = t->rq_;

    while (true)
    {
        Task task;
        rq->pop(task);

        // 处理任务
        task();
        cout << "im " << t->name_ << ",task is " << task.get_task() << " ,result is " << task.get_result() << " ,code is " << task.get_code() << endl;
        // sleep(1);
    }
}
void *product_multiple(void *args)
{
    thread *t = static_cast<thread *>(args);
    ring_queue<Task> *rq = t->rq_;
    while (true)
    {
        // 生产任务
        int x = rand() % 10 + 1;
        usleep(30);
        int y = rand() % 5;
        usleep(30);
        char op = symbol[rand() % (sym_size - 1)];
        Task task(x, y, op);

        rq->push(task);
        cout << "im " << t->name_ << ",task is " << task.get_task() << endl;
        sleep(1);
    }
}

void single()
{
    ring_queue<Task> *rq = new ring_queue<Task>(10);
    pthread_t c, p;

    pthread_create(&c, nullptr, consume_single, &rq);
    pthread_create(&p, nullptr, product_single, &rq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
}

const int p_num = 4, c_num = 2;

void multiple()
{
    ring_queue<Task> *rq = new ring_queue<Task>;
    pthread_t c[c_num], p[p_num];

    for (int i = 0; i < p_num; ++i)
    {
        string name = "producer-" + to_string(i + 1);
        thread *t = new thread(rq, name);
        pthread_create(&p[i], nullptr, product_multiple, t);
    }
    for (int i = 0; i < c_num; ++i)
    {
        string name = "consumer-" + to_string(i + 1);
        thread *t = new thread(rq, name);
        pthread_create(&c[i], nullptr, consume_multiple, t);
    }

    for (size_t i = 0; i < p_num; ++i)
    {
        pthread_join(p[i], nullptr);
    }
    for (size_t i = 0; i < c_num; ++i)
    {
        pthread_join(c[i], nullptr);
    }
}
int main()
{
    srand(time(nullptr) ^ getpid());
    // single();
    multiple();

    return 0;
}

示例

仔细看消费线程,他们消费的顺序都是按照生产的顺序来的:


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/387984.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL进阶之路】MySQL 中的分库分表方案解决方案

欢迎关注公众号&#xff08;通过文章导读关注&#xff1a;【11来了】&#xff09;&#xff0c;及时收到 AI 前沿项目工具及新技术的推送&#xff01; 在我后台回复 「资料」 可领取编程高频电子书&#xff01; 在我后台回复「面试」可领取硬核面试笔记&#xff01; 文章导读地址…

话题——程序员为什么不喜欢关电脑?

程序员为什么不喜欢关电脑&#xff1f; 方向一&#xff1a;工作流程与需求 程序员的工作往往涉及长时间、连续的任务&#xff0c;如代码编写、调试、测试等。这些任务需要高度的集中和专注&#xff0c;而频繁地关机和重启可能会打断他们的工作流&#xff0c;导致他们需要重新…

猫头虎分享已解决Bug || DNS解析问题(DNS Resolution Issue):DNSLookupFailure, DNSResolveError

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

基于决策树的金融市场波动性预测与应用

基于决策树的金融市场波动性预测与应用 项目背景与意义数据概述与分析数据来源数据特征 数据预处理与特征工程模型训练与评估结果与应用总结 LightGBM是一个机器学习算法库&#xff0c;用于梯度提升机&#xff08;Gradient Boosting Machine&#xff09;的实现。梯度提升机是一…

如何书写一个标准JavaBean

前言&#xff1a;在学习Java类的三大特征之一的封装的时候&#xff0c;对封装的数据Java有着自己已经规定好的书写格式&#xff0c;我们需要按照对应的格式进行书写。 我们大致了解一下要学习的内容&#xff1a; 1.封装的概念 如图&#xff08;看不懂没关系&#xff0c;下面会…

iTop-4412 裸机程序(二十二)- RTC时钟

目录 0.源码1. RTC2. iTop4412 中的 RTC使用的相关寄存器3. BCD编码4. 关键源码 0.源码 GitHub&#xff1a;https://github.com/Kilento/4412NoOS 1. RTC RTC是实时时钟&#xff08;Real Time Clock&#xff09;的缩写&#xff0c;是一种用于计算机系统的硬件设备&#xff0…

2024.02.12作业

1. 段错误 2. 段错误 3. hello 4. world 5. int a; int* a; int **a; int a[10]; int* a[10]; int(* a)[10]; int* a(int); int (*a[10])(int); 6. 6&#xff1b; 2&#xff1b; 2 7. 2 8. 2 9. b 10. a 11. a 12. c 13. b 14. c 15. a 16. c 17. b 18. a 19…

【2024年最新指南】掌握国内虚拟卡订阅midjourney的绝佳方法!轻松实现midjourney银行卡支付!(图文详解,简单易懂)

1.Midjourney介绍 Midjourney 是一款备受欢迎的人工智能生成图像工具&#xff0c;它可以通过输入文字描述&#xff0c;自动生成精美的图像。与许多其他图像生成工具不同&#xff0c;Midjourney 不需要安装任何软件&#xff0c;也不受个人电脑性能的限制&#xff0c;因为它运行…

「数据结构」MapSet

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;Java数据结构 &#x1f387;欢迎点赞收藏加关注哦&#xff01; Map&Set &#x1f349;概念&#x1f349;模型&#x1f349;Map&#x1f34c;TreeMap和HashMap的区别&#x1f34c;Map常用方…

第13章 网络 Page727~728 asio定时器例子:后创建的定时器先产生到点事件

代码&#xff1a; 35行&#xff0c;42行&#xff0c;51行&#xff0c;分别构造三个对象&#xff0c; 36行&#xff0c;43行&#xff0c;52行&#xff0c;设置了三个任务peng1、peng2、peng3&#xff0c;并将任务交给io_service对象&#xff08;不需要ios的run()方法启动起来&a…

算法沉淀——队列+宽度优先搜索(BFS)(leetcode真题剖析)

算法沉淀——队列宽度优先搜索&#xff08;BFS&#xff09; 01.N 叉树的层序遍历02.二叉树的锯齿形层序遍历03.二叉树最大宽度04.在每个树行中找最大值 队列 宽度优先搜索算法&#xff08;Queue BFS&#xff09;是一种常用于图的遍历的算法&#xff0c;特别适用于求解最短路径…

文件上传-第三方服务阿里云OSS

JAVA后端实现文件上传,比如图片上床功能,有很多实现方案,可以将图片保存到服务器的硬盘上。也可以建立分布式集群,专门的微服务来存储文件常见的技术比如Minio。对于中小型公司&#xff0c;并且上传文件私密性不高的话可以使用第三方的存储服务&#xff0c;比如阿里云、华为云等…

【51单片机】一个简单的例子TMOD&TCON带你永远理解【(不)可位寻址】

前言 大家好吖&#xff0c;欢迎来到 YY 滴单片机系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过单片机的老铁 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Linux》专栏YY的《数据…

【超级干货】ArcGIS_空间连接_工具详解

帮助里对空间连接的解释&#xff1a; 根据空间关系将一个要素的属性连接到另一个要素。 目标要素和来自连接要素的被连接属性写入到输出要素类。 如上图所示&#xff0c;关键在于空间关系&#xff0c;只有当两个要素存在空间关系的时候&#xff0c;空间连接才有用武之地。 一…

网站被劫持了怎么解决

网站被劫持是一种常见的网络安全问题&#xff0c;它通常表现为用户访问网站时被自动跳转到其他页面&#xff0c;这不仅影响用户体验&#xff0c;还可能对网站带来负面影响。面对这种情况&#xff0c;如何运用高技术手段来有效应对和防范网站劫持&#xff0c;成为了一个迫切需要…

【Linux学习】线程池

目录 23.线程池 23.1 什么是线程池 23.2 为什么需要线程池 23.3 线程池的应用场景 23.4 实现一个简单的线程池 23.4.1 RAII风格信号锁 23.4.2 线程的封装 23.4.3 日志打印 22.4.4 定义队列中存放Task类任务 23.4.5 线程池的实现(懒汉模式) 为什么线程池中需要有互斥锁和条件变…

MySQL学习记录——구 复合查询

文章目录 1、基本查询2、多表查询3、自连接4、子查询1、多行子查询2、多列子查询3、from句中的子查询 5、合并查询 1、基本查询 看一些例子&#xff0c;不关心具体内容&#xff0c;只看写法 //查询工资高于500或岗位为MANAGER的雇员, 同时还要满足他们的姓名首字母为大写的J …

Java图形化界面编程——AWT概论 笔记

2.3 Container容器 2.3.1 Container继承体系 Winow是可以独立存在的顶级窗口,默认使用BorderLayout管理其内部组件布局;Panel可以容纳其他组件&#xff0c;但不能独立存在&#xff0c;它必须内嵌其他容器中使用&#xff0c;默认使用FlowLayout管理其内部组件布局&#xff1b;S…

DOM事件练习1

DOM事件练习1 1. 演示效果 2. 分析思路 用 ul 创建四个 li 列表整个列表的背景是红色的&#xff0c;鼠标悬浮在列表上&#xff0c;一行的变为蓝色点击任意列表&#xff0c;整个列表的背景变为白色&#xff0c;被点击的列表变为粉色需要用到 js 的点击事onclick件和forEach循环…

【并发编程】ThreadPoolExecutor类

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;并发编程⛺️稳重求进&#xff0c;晒太阳 ThreadPoolExecutor 1) 线程池状态 ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态&#xff0c;低 29 位表示线程数量 状态名 高三位 …