linux篇【11】:linux下的线程<后序>

news2024/11/19 14:54:45

目录

一.线程互斥

1.三个概念

2.互斥

(1)在执行语句的任何地方,线程可能被切换走

(3)抢票场景中的问题

(4)解决方案

3.加锁

(1)加锁介绍

(2)定义/释放 互斥锁

(3)加锁、解锁(使用锁)

(4)使用锁代码

4.加锁方式

(1)基本的传锁(就传不了线程名字了)

(2)既传name又传锁

二.加锁的原理探究 

1.上锁后的临界区内仍可以进程切换。

2.在我被切走的时候,绝对不会有线程进入临界区!

3.加锁是原子的

(1)xchgb 交换是原子的

(2)加锁原理

(3)C++ 加锁

Makefile

Lock.hpp

mythread.cc

(4)C++ RAII加锁

4.可重入VS线程安全

5. 常见锁概念

(1)死锁


一.线程互斥

1.三个概念

1.临界资源:多个执行流都能看到并能访问的资源,临界资源
2.临界区:多个执行流代码中有不同的代码,访问临界资源的代码,我们称之为临界区
3.互斥特性:当我们访问某种资源的时候,任何时刻。都只有一个执行流在进行访问,这个就叫做:互斥特性

4.线程互斥:线程互斥 指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性

2.互斥

没有互斥时,以抢票为例,一抢票 票数减1: int tickets;   tickets--;为例

(1)在执行语句的任何地方,线程可能被切换走

int tickets;
tickets--;tickets--是由3条语句完成的:

tickets--:有三步
① load tickets to reg
② reg-- ;
③ write reg to tickets

(2)CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。线程被切换的时候,需要保存上下文;线程被换回的时候,需要恢复上下文。 

(3)抢票场景中的问题

情况1:线程A先抢到一张票时,寄存器中tickets 10000——>9999, 还未写回内存,A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,直接抢了9950张,还剩50张,此时B的时间片到了,又切回线程A,又把9999写入内存,就错误了。

        if (tickets > 0)
        {
            usleep(1000);
            cout << name << " 抢到了票, 票的编号: " << tickets << endl;
            tickets--;
            usleep(123); //模拟其他业务逻辑的执行
        }

情况2:或者在抢最后一张时,线程A先抢最后一张票,if (tickets > 0)为真,进入if语句,此时A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,此时显示票数仍是1,if (tickets > 0)为真,进入if语句,并执行tickets--;,tickets变为0,此时B的时间片到了,又切回线程A,线程A又继续执行tickets--;,此时直接把票数减到了负数,就出错了。

(4)解决方案

原子性:一件事要么不做,要么全做完

把tickets--这个临界区设为原子的,使不想被打扰,加锁

3.加锁

(1)加锁介绍

加锁范围:临界区,只要对临界区加锁,而且加锁的力度越细越好

加锁本质:加锁的本质是让线程执行临界区代码串行化

加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加

锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!那这把锁,本身不就也是临界资源吗?锁的设计者早就想到了

pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!申请锁的过程不会中断,不会被打扰。

难度在加锁的临界区里面,就没有线程切换了吗????

mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:

  • 0表示已经有执行流加锁成功,资源处于不可访问,
  • 1表示未加锁,资源可访问。

(2)定义/释放 互斥锁

man pthread_mutex_init

① pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 定义全局/静态的互斥锁,可以用这个宏初始化

② int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);         mutex:锁的地址。attr:锁的属性设为空             

③ int pthread_mutex_destroy(pthread_mutex_t *mutex); 释放锁

(3)加锁、解锁(使用锁)

man pthread_mutex_lock

① int pthread_mutex_lock(pthread_mutex_t *mutex);  加阻塞式锁

线程1正在用锁住的代码,那线程2就要阻塞式等待线程1执行完才能使用这个锁(即执行锁住的代码)

② int pthread_mutex_trylock(pthread_mutex_t *mutex); 加非阻塞式锁

线程1正在用这个非阻塞锁(即执行锁住的代码),那线程2就直接返回,只有当没有别的线程用这个锁,自己才能用。

③ int pthread_mutex_unlock(pthread_mutex_t *mutex);  解锁

如果不解锁,比如线程1使用锁后没有解锁就退出了,那么其他线程在竞争使用这个锁时,就会一直处于休眠,等待这个锁被解锁才能继续使用, n = pthread_join(tid4, nullptr); pthread_join就会一直阻塞等待线程退出,所以会显示进程卡住了。

(4)使用锁代码

else那里也要解锁,否则会阻塞:线程1走else使用锁后如果没有解锁就退出了,那么其他线程在竞争使用这个锁时,就会一直处于休眠,等待这个锁被解锁才能继续使用, n = pthread_join(tid4, nullptr); pthread_join就会一直阻塞等待线程退出,所以会显示进程卡住了。

跟上面代码一样(可以忽略):

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h> // 仅仅是了解

// __thread int global_value = 100;

// void *startRoutine(void *args)
// {
//     // pthread_detach(pthread_self());
//     // cout << "线程分离....." << endl;
//     while (true)
//     {
//         // 临界区,不是所有的线程代码都是临界区
//         cout << "thread " << pthread_self() << " global_value: "
//              << global_value << " &global_value: " << &global_value
//              << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid)<<endl;
//         sleep(1);
//         break;
//     }
//     // 退出进程,任何一个线程调用exit,都表示整个进程退出
//     //exit(1);
//     // pthread_exit()
// }

using namespace std;

// int 票数计数器
// 临界资源
int tickets = 10000; // 临界资源,可能会因为共同访问,可能会造成数据不一致问题。
pthread_mutex_t mutex;

void *getTickets(void *args)
{
    const char *name = static_cast<const char *>(args);

    while (true)
    {
        // 临界区,只要对临界区加锁,而且加锁的粒度约细越好
        // 加锁的本质是让线程执行临界区代码串行化
        // 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加
        // 锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!
        // 这把锁,本身不就也是临界资源吗?锁的设计者早就想到了
        // pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!
        // 难度在加锁的临界区里面,就没有线程切换了吗????
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            usleep(1000);
            cout << name << " 抢到了票, 票的编号: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(&mutex);

            //other code
            usleep(123); //模拟其他业务逻辑的执行
        }
        else
        {
            // 票抢到几张,就算没有了呢?0
            cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
            pthread_mutex_unlock(&mutex);
            break;
        }
    }

    return nullptr;
}

// 如何理解exit?
int main()
{
    pthread_mutex_init(&mutex, nullptr);
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;
    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");
    pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");

    // sleep(1);
    // 倾向于:让主线程,分离其他线程

    // pthread_detach(tid1);
    // pthread_detach(tid2);
    // pthread_detach(tid3);

    // 1. 立即分离,延后分离 -- 线程活着 -- 意味着,我们不在关心这个线程的死活。4. 线程退出的第四种方式,延后退出
    // 2. 新线程分离,但是主线程先退出(进程退出) --- 一般我们分离线程,对应的main thread一般不要退出(常驻内存的进程)
    // sleep(1);

    int n = pthread_join(tid1, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid2, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid3, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid4, nullptr);
    cout << n << ":" << strerror(n) << endl;

    pthread_mutex_destroy(&mutex);

    return 0;
}

4.加锁方式

(1)基本的传锁(就传不了线程名字了)

#include <unistd.h>
#include <pthread.h>
#include <iostream>
using namespace std;
int tickets = 1000;
void *startRoutine(void *args)
{
    pthread_mutex_t* mutex_p= static_cast<pthread_mutex_t*>(args);
    while (true)
    {
        pthread_mutex_lock(mutex_p);//如果申请不到,线程阻塞等待
        if (tickets > 0)
        {
            usleep(1000);
            cout << "thread: " << pthread_self() << "get a ticket: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(mutex_p);
            //做其他的事
            usleep(500);
        }
        else
        {
            pthread_mutex_unlock(mutex_p);
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3, t4;

    static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    pthread_create(&t1, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t2, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t3, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t4, nullptr, startRoutine, (void *)&mutex);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_mutex_destroy(&mutex);
    return 0;
}

 

 

(2)既传name又传锁

完整版:

#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <iostream>
using namespace std;
int tickets = 1000;
#define NAMESIZE 64
typedef struct threadData
{
    char name[NAMESIZE];
    pthread_mutex_t* mutexp;
}threadData;
void *startRoutine(void *args)
{
    threadData* td= static_cast<threadData*>(args);
    while (true)
    {
        pthread_mutex_lock(td->mutexp);//如果申请不到,线程阻塞等待
        if (tickets > 0)
        {
            usleep(1000);
            cout << "thread: " << td->name << "get a ticket: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(td->mutexp);
            //做其他的事
            usleep(500);
        }
        else
        {
            pthread_mutex_unlock(td->mutexp);
            break;
        }
    }
    return nullptr;
}

int main()
{
    static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    pthread_t t1, t2, t3, t4;

    threadData *td1=new threadData();
    threadData *td2=new threadData();
    threadData *td3=new threadData();
    threadData *td4=new threadData();
    strcpy(td1->name,"thread 1");
    strcpy(td2->name,"thread 2");
    strcpy(td3->name,"thread 3");
    strcpy(td4->name,"thread 4");
    td1->mutexp=&mutex;
    td2->mutexp=&mutex;
    td3->mutexp=&mutex;
    td4->mutexp=&mutex;
    pthread_create(&t1, nullptr, startRoutine, (void *)td1);
    pthread_create(&t2, nullptr, startRoutine, (void *)td2);
    pthread_create(&t3, nullptr, startRoutine, (void *)td3);
    pthread_create(&t4, nullptr, startRoutine, (void *)td4);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_mutex_destroy(&mutex);
    return 0;
}

 

 

二.加锁的原理探究 

1.上锁后的临界区内仍可以进程切换。

我在临界资源对应的临界区中上锁了,临界区还是多行代码,是多行代码就可以被切换。加锁 不等于 不会被切换。加锁后仍然可以切换进程,因为线程执行的加锁解锁等对应的也是代码,线程在任意代码处都可以被切换,只是线程加锁是原子的——要么你拿到了锁,要么没有

2.在我被切走的时候,绝对不会有线程进入临界区!

——因为每个线程进入临界区都必须先申请锁! !假设当前的锁被A申请走了,即便当前的线程A没有被调度,因为它是被切走的时候是抱着锁走的,其他线程想进入临界区需要先申请锁,但是已经有线程A持有锁了,则其他线程在申请时会被阻塞。即:一旦一个线程持有了锁,该线程根本就不担心任何的切换问题!对于其他线程而言,线程A访问临界区,只有没有进入和使用完毕两种状态
,才对其他线程有意义!即:对于其他线程而言,线程A访问临界区具有一定的原子性
注意:尽量不要在临界区内做耗时的事情!因为只有持有锁的线程能访问,其他线程都会阻塞等待。

3.加锁是原子的

①每一个CPU任何时刻只能有一个线程在跑

②单独的一条汇编代码是具有原子性的

(1)xchgb 交换是原子的

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题 为了实现互斥锁操作,大多数体系结构(芯片体系结构)都提供了swap或exchange指令,该指令的作用是使用一条汇编代码把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

(2)加锁原理

mutex中的值默认是1
%al :CPU中的寄存器( 凡是在寄存器中的数据,全部都是线程的内部上下文! !
mutex :内存中的一个变量

 

加锁原理解释:线程A执行 movb $0,%al  :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是1)  的值,交换后 寄存器%al中是1, 变量mutex中是0。还未执行判断,此时突然进程切换,线程A会自动带走%al中的上下文数据1,线程B开始执行:线程B执行 movb $0,%al  :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是0)  的值,交换后 寄存器%al 和 变量mutex中都是0。再判断——>因为%al是0,不大于0就挂起。此时线程B挂起,该线程A继续执行,线程A会把自己上下文数据恢复到%al中,此时%al=1,该执行判断了——>因为%al是1,就返回。这样就成功做到:多个线程看起来同时在访问寄存器,但是互不影响

lock和unlock的伪代码:

(3)C++ 加锁

Makefile

mythread:mythread.cc
	g++ -o $@ $^ -lpthread -std=c++11

.PHONY:clean
clean:
	rm -f mythread

Lock.hpp

#pragma once

#include <iostream>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&lock_, nullptr);
    }
    void lock()
    {
        pthread_mutex_lock(&lock_);
    }
    void unlock()
    {
        pthread_mutex_unlock(&lock_);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&lock_);
    }

private:
    pthread_mutex_t lock_;
};

class LockGuard
{
public:
    LockGuard(Mutex *mutex) : mutex_(mutex)
    {
        mutex_->lock();
        std::cout << "加锁成功..." << std::endl;
    }

    ~LockGuard()
    {
        mutex_->unlock();
        std::cout << "解锁成功...." << std::endl;
    }

private:
    Mutex *mutex_;
};

mythread.cc



int tickets = 1000;
Mutex mymutex;

// 函数本质是一个代码块, 会被多个线程同时调用执行,该函数被重复进入 - 被重入了
bool getTickets()
{
    bool ret = false; // 函数的局部变量,在栈上保存,线程具有独立的栈结构,每个线程各自一份
    LockGuard lockGuard(&mymutex); //局部对象的声明周期是随代码块的!
    if (tickets > 0)
    {
        usleep(1001); //线程切换了
        cout << "thread: " << pthread_self() << " get a ticket: " << tickets << endl;
        tickets--;
        ret = true;
    }
    cnt++;
    return ret;
}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    while(true)
    {
        if(!getTickets())
        {
            break;
        }
        cout << name << " get tickets success" << endl;
        //其他事情要做
        sleep(1);
    }
}

int cnt = 10000;

int main()
{
    pthread_t t1, t2, t3, t4;

    pthread_create(&t1, nullptr, startRoutine, (void *)"thread 1");
    pthread_create(&t2, nullptr, startRoutine, (void *)"thread 2");
    pthread_create(&t3, nullptr, startRoutine, (void *)"thread 3");
    pthread_create(&t4, nullptr, startRoutine, (void *)"thread 4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
}

(4)C++ RAII加锁

通过RAII思想,创建对象时加锁,出代码块时解锁

    {
        //临界资源
        LockGuard LockGuard(&mymutex);
        cnt++;
        ...
        ...
        ...
    }

4.可重入VS线程安全

(1)概念

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。(我们写的不加锁的抢票函数就是线程不安全函数,因为可能抢票抢到-1)
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。(90%函数是不可重入函数,带_r是可重入函数,不带_r是不可重入函数)

(2)####常见的线程不安全的情况

不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
(3)常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
(4)常见不可重入的情况
调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的
调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
(5)常见可重入的情况
不使用全局变量或静态变量
不使用用 malloc 或者 new 开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
(6)可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
(7)可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

5. 常见锁概念

(1)死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
### 死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件 : 一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件 : 若干执行流之间形成一种头尾相接的循环等待资源的关系
### 避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
### 避免死锁算法
死锁检测算法 ( 了解 )
银行家算法(了解)
##7. Linux 线程同步
### 条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情
况就需要用到条件变量。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp"
#include <mutex>

using namespace std;

pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;

void *startRoutine1(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&mutexA);
        sleep(1);
        pthread_mutex_lock(&mutexB);

        cout << "我是线程1,我的tid: " << pthread_self() << endl;

        pthread_mutex_unlock(&mutexA);
        pthread_mutex_unlock(&mutexB);
    }
}
void *startRoutine2(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&mutexB);
        sleep(1);
        pthread_mutex_lock(&mutexA);

        cout << "我是线程2, 我的tid: " << pthread_self() << endl;

        pthread_mutex_unlock(&mutexB);
        pthread_mutex_unlock(&mutexA);
    }
}

int main()
{
    pthread_t t1, t2;

    pthread_create(&t1, nullptr, startRoutine1, nullptr);
    pthread_create(&t2, nullptr, startRoutine2, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言百日千题系列之《忘情水题》第一日

目录 绪论 1.最大数位置 2.与指定数字相同的数的个数 3.蓝桥杯2013年第四届真题-核桃的数量 4.求所给范围内水仙花数并排列 5.最大值和最小值的差 6.计算书费 7.角谷猜想 8. 最高的分数 9.年龄与疾病 10.-百钱百鸡问题 绪论 本文是C语言百日千题系列《忘情水题》的第…

BCN衍生物:endo-BCN-PEG4-TAMRA,endo-BCN-PEG4-Palmitic,endo-BCN-PEG4-DSPE的特点分享

凯新生物公司小编分享&#xff1a;endo-BCN-PEG4-TAMRA &#xff0c;endo-BCN-PEG4-Palmitic&#xff0c;endo-BCN-PEG4-DSPE这几种的物理相关数据。 1、endo-BCN-PEG4-TAMRA 四甲基罗丹明&#xff08;TAMRA&#xff09;-叠氮化物是一种化学探针&#xff0c;用于直接在活细胞中…

代码随想录算法训练营第八天|二叉树(截止到左叶子之和)

翻转二叉树 Leecode 226.翻转二叉树 链接&#xff1a;https://leetcode.cn/problems/invert-binary-tree/ 用递归来做&#xff0c;若是遇到空节点&#xff0c;直接return 然后交换左右节点&#xff0c;接着递归 class Solution { public:TreeNode* invertTree(TreeNode* r…

Java代码审计——SSH 框架审计技巧

目录 &#xff08;一&#xff09; SSH 框架简介 &#xff08;二&#xff09; Java SSH 框架审计技巧 &#xff08;一&#xff09; SSH 框架简介 上个月介绍了 SSM 框架&#xff0c;即 Spring MVC、Spring 和 MyBatis。接下来介绍 Java Web曾经开发的 SSH 框架&#xff0c;即 …

河北涿州水稻种植历史 国稻种芯·中国水稻节:保定效益双赢

河北涿州水稻种植历史 国稻种芯中国水稻节&#xff1a;保定效益双赢 央视网消息 保定日报讯&#xff08;通讯员张千 刘永兴 王蕾&#xff09;新闻中国采编网 中国新闻采编网 谋定研究中国智库网 中国农民丰收节国际贸易促进会 国稻种芯中国水稻节 中国三农智库网-功能性农业农…

浅谈前缀索引

一.什么是前缀索引 所谓前缀索引说白了就是对字符串或前n个字符建立索引 二.为什么选择前缀索引 一般来说使用前缀索引&#xff0c;可能都是因为整个字段的数据量太大&#xff0c;没有必要针对整个字段建立索引&#xff0c;前缀索引仅仅是选择一个字段的前n个字符作为索引&a…

Linux运维工程师的操作规范

&#xff0c;Linux运维工程师的操作规范从事运维有一段时间了&#xff0c;遇到过各式各样的问题&#xff0c;数据丢失&#xff0c;网站挂马&#xff0c;误删数据库文件&#xff0c;黑客攻击等各类问题。 今天简单整理一下&#xff0c;分享给各位小伙伴。 一、线上操作规范 1、…

你可见过如此细致的延时任务详解

概述 延时任务相信大家都不陌生&#xff0c;在现实的业务中应用场景可以说是比比皆是。例如订单下单 15 分钟未支付直接取消&#xff0c;外卖超时自动赔付等等。这些情况下&#xff0c;我们该怎么设计我们的服务的实现呢&#xff1f; 笨一点的方法自然是定时任务去数据库进行轮…

华为机试 - 滑动窗口最大和

目录 题目描述 输入描述 输出描述 用例 题目解析 算法源码 题目描述 有一个N个整数的数组&#xff0c;和一个长度为M的窗口&#xff0c;窗口从数组内的第一个数开始滑动直到窗口不能滑动为止&#xff0c; 每次窗口滑动产生一个窗口和&#xff08;窗口内所有数的和&…

常用的框架技术-09 Spring Security Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录1.Spring Security简介1.1 Spring Security概述1.2 Spring Security历史发展1.3 产品的对比1.3.1 Spring Security1.3.2 Shiro1.4 Spring Security 核心类1.4.1 Auth…

qemu 线程 vhost

[rootlocalhost cloud_images]# lsmod | grep vhost_net vhost_net 262144 0 vhost 262144 1 vhost_net tap 262144 1 vhost_net tun 262144 2 vhost_net [rootlocalhost cloud_images]#vhost-net网卡的…

[附源码]SSM计算机毕业设计基于实时定位的超市配送业务管理JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

低碳世界杂志低碳世界杂志社低碳世界编辑部2022年第7期目录

节能环保 挥发性有机物的全厂控制措施 董少军; 1-3 《低碳世界》投稿&#xff1a;cnqikantg126.com 佛山市市政排水管网通沟污泥处理处置工艺设计 张红; 4-6 “双碳”背景下海岸带地区适应气候变化评估与对策研究 王鸿浩;邬乐雅;吴晓晨;张丽佳;黄婧蓼琦;胡斐…

【毕业设计】基于情感分析的网络舆情热点分析系统

文章目录0 前言1 课题背景2 数据处理3 文本情感分析3.1 情感分析-词库搭建3.2 文本情感分析实现3.3 建立情感倾向性分析模型4 数据可视化工具4.1 django框架介绍4.2 ECharts5 Django使用echarts进行可视化展示5.1 修改setting.py连接mysql数据库5.2 导入数据5.3 使用echarts可视…

Java编程实战9:统计只差一个字符的子串数目

目录统计只差一个字符的子串数目题目示例 1示例 2示例 3示例 4提示解答解题思路完整代码统计只差一个字符的子串数目 题目 给你两个字符串 s 和 t &#xff0c;请你找出 s 中的非空子串的数目&#xff0c;这些子串满足替换 一个不同字符 以后&#xff0c;是 t 串的子串。换言…

实验1:Arduino的nRF24L01单向收发实验

实验结果: 发送端发送“Hello World”,发送成功打印1 接收端接收到“Hello World”,在串口中打印出“Hello World” OK,直接讲代码 因为我用的Arduino和nRF24L01 是用扩展板连接的,而我的嵌入式硬件开发,也就是AD实在不擅长,就不解释了 其中(9,10)CE,CSN 那么我…

通关算法题之 ⌈数组⌋ 下

二分搜索 704. 二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 输入: nums [-1,0,3,5,9,12], target 9 输出…

【后台技术】异步编程指北,问题和重点

导语&#xff1a;同步、异步&#xff0c;并发、并行、串行&#xff0c;这些名词在我们的开发中会经常遇到&#xff0c;这里对异步编程做一个详细的归纳总结&#xff0c;希望可以对这方面的开发有一些帮助。 内容大纲&#xff1a; 1、几个名词的概念 多任务的时候&#xff0c;…

jmeter压力测试报告

出版社智能智造测试报告 &#xff08;二期版本&#xff09; 2022年11月 目 录 1. 测试背景 1.1. 项目背景 1.2. 测试目的 1.3. 测试时间 1.4. 测试资源 1.5. 参考资料 2. 测试范围 3. 性能需求指标 3.1. 业界指标 4. 测试工具 5. 测试环境 5.1. 阿里云测试环境软…

搭建Gitlab

Gitlab是目前被广泛使用的基于git的开源代码管理平台, 基于Ruby on Rails构建, 主要针对软件开发过程中产生的代码和文档进行管理 一、搭建gitlab服务器&#xff0c;统一管理软件项目 第一步&#xff1a; 创建一个4G内存的虚拟机&#xff0c;否则很容易启动不了&#xff0c;报…