Linux 多线程( 进程VS线程 | 线程控制 )

news2024/12/26 21:56:39

文章目录

  • Linux进程 VS 线程
    • 进程的多个线程共享
  • 进程和线程的关系
    • 线程创建 pthread_create
    • 获取线程ID pthread_self
    • 线程等待 pthread_join
    • 终止线程
    • 进程分离
    • 线程ID及进程地址空间布局

Linux进程 VS 线程

  1. 进程是资源分配的基本单位。
  2. 线程是OS调度的基本单位。

线程共享进程数据,但也拥有自己的一部分数据:

  • 线程ID
  • 一组寄存器,用来保存每个线程的上下文数据,让每个线程能够合理调度。
  • ,每个线程入栈出栈产生的临时变量必须保存到每个线程的私有栈中,所以栈对于每个线程来说也是私有的。
  • errno
  • 信号屏蔽字
  • 调度优先级

进程的多个线程共享

因为在在同一个地址空间,所以所谓的代码段,数据段都是共享的。

  • 如果定义一个函数,各个线程都可以调用。
  • 如果定义一个全局变量,那么一个进程中的多个执行流都可以访问到。

除此之外,各线程还共以下资源和环境:

  • 文件描述符 ( 进程打开一个文件,其他线程也能够看到并访问。
  • 各种信号的处理方式了。( SIG_IGN,SIG_DFL 等默认处理的信号函数 或者 自定义的信号处理函数).
  • 当前工作目录
  • 用户ID和组ID。

进程和线程的关系

进程和线程的关系,例如:

在这里插入图片描述
之前,我们都是以单线程进程学习为主,以后我们也将尝试解除单进程多线程学习。

线程创建 pthread_create

创建线程的函数为pthread_create,原型如下:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

参数说明

  • thread:获取创建成功的线程ID,该参数是一个输出型参数。
  • attr:用于设置创建线程的属性,传入NULL表示使用默认属性。
  • start_routine:返回值和参数均为void*的函数指针。该参数表示线程例程,即线程启动后要执行的函数。
  • arg:传给线程例程的参数。

返回值说明

  • 线程创建成功返回0,失败返回错误码。

注意
Linux不能真正意义上的帮我们提供线程的接口,但是Linux有原生线程库,使用此函数必须在编译时带上 -pthread 选项。

以下例子中,我们让主线程创建一个新线程,预计主线程与新线程分别去执行对应的函数代码。

void* Routine(void* arg)
{
	char* msg = (char*)arg;
	while (1){
		cout << " i am a thread 1 " << endl;
		sleep(1);
	}
}
int main()
{
	pthread_t tid;
	pthread_create(&tid, NULL, Routine, (void*)"thread 1");
	while (1){
	    cout << " I am a main thread " << endl;
		sleep(2);
	}
	return 0;
}

结果如下:
在这里插入图片描述
当然我们也可以使用 ps -ajx 命令来查看当前进程信息,但是,使用该命令只查到了mythread进程相关信息,没有显示其他的线程。
在这里插入图片描述
所以,我们可以使用 ps -aL 命令,来显示当前进程中的线程信息。其中LWP( Light Weight Process )就是代表该线程的ID,可以看到,这两个线程的PID是一样的,就代表它们同属于一个进程。
在这里插入图片描述
我们以前学习进程的时候认为OS调度的时候以PID为准,实际上OS调度的时候采用的是PWD,只不过主线程的PWD和PID是一样的,所以单线程进程调度时采用PID和PWD实际上是一样的。

获取线程ID pthread_self

我们可以调用pthread_self函数获取线程PWD。

函数原型如下

pthread_t pthread_self(void);

以下代码,我们通过pthread_self函数分别打印主线程和新线程的PID和PWD。

void *threadRun( void *args )
{
    const string name = ( char * )args;
    
    int count = 0;

    while( count < 5 )
    {
        cout << name << " pid: " << getpid()  << " PWd "<< pthread_self()<<  endl;
            
        sleep(1);

        ++count;
    }
    return nullptr;
}
int main()
{
    pthread_t tid[5];

    char name[64];

    for ( long long i = 0; i < 5; ++i )
    {
        snprintf( name, sizeof name, "%s - %d", "thread", i );

        pthread_create( tid + i,NULL,threadRun, (void *)name );

        sleep(1);
    }
     
    cout << " i am a main thread " << " getpid: " << getpid() << " PWD " << pthread_self() << endl;
    return 0;
}

结果如下:
在这里插入图片描述

线程等待 pthread_join

首先,我们应该注意的是,一个线程被创建出来,这个线程就如同进程一般,也是需要被等待的。如果主线程不对新线程进程等待,那么新线程资源是不会被回收的。此时,便有了pthread_join函数专门对新线程处理。

函数原型如下

int pthread_join(pthread_t thread, void **retval);

参数说明

  • thread: 被等待的线程ID。
  • retal:该retval为二级指针,一级指针指向线程的返回值。

返回值说明
线程等待成功返回0,失败返回错误码。

  • 如果thread线程通过return返回,retal所指向的单元里存放的是thread线程函数的返回值。

  • 如果thread线程被别的线程调用pthread_ cancel异常终掉,retal所指向的单元里存放的是常数PTHREAD_ CANCELED,该常数值为-1。

  • 如果thread线程是自己调用pthread_exit终止的,retal所指向的单元存放的是传给pthread_exiit的参数。

  • 如果对thread线程的终止状态不感兴趣,可以传NULL给retal参数。

例如,以下代码主线程创建一个新线程后,阻塞等待新线程打印10次后退出,主线程也随之退出。


void* threadRoutine( void* args )
{
    int i = 0;
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        if( i++ == 10 ) break;
    }

    cout << "new thread quit... " << endl;

    return nullptr;
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    pthread_join( tid,nullptr );
    
    cout<< " main thread wait done ... main quit " << endl;
}

结果如下:
在这里插入图片描述

pthread_join第二个参数

当新线程退出后,我们可以对新线程返回值设置特定值,但是需要将该值以地址的形式返回。新线程退出时,由主线程中的ret指针保存,但是如果需要改变一级指针保存的数据需要传入二级指针(ret的地址)才能获取到ret进而改变。

void* threadRoutine( void* args )
{
    int i = 0;
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        if( i++ == 10 ) break;
    }

    cout << "new thread quit... " << endl;

    return (void*)10;
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    void* ret = nullptr;

    pthread_join( tid,&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " <<  (long long )ret<<  endl;
}

结果如下:

在这里插入图片描述
我们知道,每个线程的栈是私有的,但是我们也可以通过 pthread_join第二个参数来获取,这更加体现了主新线程之间的数据传输。
例如: 我们在threadRoutine例程中创建了一个数组,并通过返回值返回由ret指针接受。

void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
    }

    cout << "new thread quit... " << endl;

    return (void*)data;
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    //cout<< " main thread wait done ... main quit " << " exitcode: " <<  endl;

    for( int i = 0; i < 10; i++  )
    {
        cout << ret[i] << endl;
    }
    return 0;
}

结果如下:
在这里插入图片描述

线程出现异常吗,整个进程也出现异常。

在以上的代码中,我们在例程中写出除0错误,当该线程崩溃时,整个进程也将随即崩溃,此时再获取线程的退出码也没有意义。

void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
         
        int a = 100;
        
        a /= 0;
        
    }

    cout << "new thread quit... " << endl;

    return (void*)data;
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    //cout<< " main thread wait done ... main quit " << " exitcode: " <<  endl;

    for( int i = 0; i < 10; i++  )
    {
        cout << ret[i] << endl;
    }
    return 0;
}

结果如下:
在这里插入图片描述

终止线程

如果需要只终止某个线程而不是终止整个进程,可以有三种方法:

  • 从线程函数return。
  • 线程可以自己调用pthread_exit函数终止自己。
  • 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程。

终止线程pthread_exit

pthread_exit函数的功能就是终止线程,pthread_exit函数的函数原型如下:

void pthread_exit(void *retval);

参数说明
retval:线程退出时的退出码信息。

例如: 我们使用Pthread_exit函数终止进程,并将退出码设为10。

void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
         
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " << ( long long ) ret  <<  endl;
    
    return 0;
}

结果如下
在这里插入图片描述

注意
exit函数的作用是终止进程,任何一个线程调用exit函数也代表的是整个进程终止。

终止进程 pthread_cancel

我们可以通过pthread_cancel函数取消某一个线程,该函数原型如下:

int pthread_cancel(pthread_t thread);

参数说明

thread:被取消线程的ID。

返回值说明

线程取消成功返回0,失败返回错误码。

例如: 我们让新线程执行一段时间,随后主线程调用pthread_cancel函数取消该新线程,我们一般都是由主线程取消新线程,( 这是pthread_cancel 的常规用法 )


void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
         
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int count = 0;
    
    while( true )
    {
        cout << "main线程: " << "running..." << endl;
        sleep(1);
        count++;
        if( count >= 5 ) break;
    }
    pthread_cancel(tid);

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " << ( long long ) ret  <<  endl;
    
    return 0;
}

结果如下
我们可以看出,此时的新线程返回值不再是我们原先设置的10,因为该新线程是由pthread_cancel函数取消终止的,OS默认设置其返回值为-1.
在这里插入图片描述

进程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

pthread_detach函数原型如下:

int pthread_detach(pthread_t thread);

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离,但是常规情况下,我们一般让新线程自己分离。

void* threadRoutine( void* args )
{
    pthread_detach(pthread_self());
    while( true )
    {
        cout << "新线程: " << ( char* )args << endl;
    
        sleep(1);
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int count = 0;
    
    while( true )
    {
        cout << " main 线程 " << endl;
        sleep(1);
        count++;
        if( count >= 5 ) break;
    }
    
    cout<< " main thread wait done ... main quit " <<  endl;
    
    return 0;
}

注意
joinable和分离是冲突的,一个线程不能既是joinable又是分离的,并且在常规线程分离的场景中,主线程一般用来创建新线程处理任务和回收资源,一般都是最后退出的。如果主线程先退出,就意味着进程退出,那么新线程也立刻会随即退出。

线程ID及进程地址空间布局

线程ID本质上是一个地址

  • pthread_read函数会产生一个线程ID,存放在第一个参数指向的地址中,但是线程ID与前面所说的线程ID LWP 不同。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID。

当进程运行时,pthread共享库即加载到物理内存中,再根据页表,映射到进程地址空间中的共享区。

在这里插入图片描述

主线程和新线程都含有各自的独立栈结构来保存每一个线程都是独立的,主线程用的是内核级的栈结构,每一个新线程都含有共享区中独有的pthread库中的栈结构。为了对这些属性数据进行管理,OS采用了“先描述,再组织”的方式,该动态库中包含了一个个struct pthread结构体,其中包含了线程栈,上下文等数据,而线程ID(tid)便是动态库中每一个struct pthread结构体的首地址,进而CPU通过tid来找到对应的线程。

在这里插入图片描述

打印线程ID

我们现在可以对线程ID进行打印。

void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " running... " << endl;
        sleep(1);
        data[i] = i;
        if( i++ == 10 ) break;
         
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    printf( " %lu , %p \n ",tid,tid );

    int count = 0;
    
    while( true )
    {
        cout << "main线程: " << "running..." << endl;
        sleep(1);
        count++;
        if( count >= 5 ) break;
    }
    pthread_cancel(tid);

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " << ( long long ) ret  <<  endl;
    
    return 0;
}

结果如下
可见,线程ID本质上就是一个地址。
在这里插入图片描述

线程的局部存储

我们知道,全局变量,已初始化数据,未初始化数据等都是线程间共享的。但是,我们可以在全局变量前添加__pthread 代表每一个线程都含有该独有的全局变量保存在每一个线程局部存储变量中。

例如: 我们分别通过主线程和新新线程打印全局变量g_val的值和地址。

__thread  int g_val = 0;
void* threadRoutine( void* args )
{
    int i = 0;
    int* data = new int[11];
    while( true )
    {
        cout << "新线程: " << ( char* )args << " g_val: " << g_val <<  " &g_val "  << &g_val <<  endl;
        
        ++g_val;
              
        sleep(1);
    }

    cout << "new thread quit... " << endl;

   pthread_exit((void*)10);
}

int main()
{
    pthread_t tid;
    
    pthread_create( &tid,nullptr,threadRoutine,(void*)"thread 1 ");

    int count = 0;
    
    while( true )
    {
        cout << "main线程: " << " g_val " << g_val << " &g_val " << &g_val <<  endl;
        sleep(1);
        count++;
        if( count >= 5 ) break;
    }
    pthread_cancel(tid);

    int* ret = nullptr;

    pthread_join( tid,(void**)&ret );
    
    cout<< " main thread wait done ... main quit " << " exitcode: " << ( long long ) ret  <<  endl;
    
    return 0;
}

结果如下:
我们可以看到,主线程g_val值没有变化,而新线程g_val每一次打印都增加了1,并且主新线程中的g_val的地址是不同的。
在这里插入图片描述

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

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

相关文章

Michael.W基于Foundry精读Openzeppelin第34期——MerkleProof.sol

Michael.W基于Foundry精读Openzeppelin第34期——MerkleProof.sol 0. 版本0.1 MerkleProof.sol 1. 目标合约2. 代码精读2.1 processProof(bytes32[] memory proof, bytes32 leaf) && processProofCalldata(bytes32[] calldata proof, bytes32 leaf)2.2 verify(bytes32[…

图像处理之频域滤波DFT

摘要&#xff1a;傅里叶变换可以将任何满足相应数学条件的信号转换为不同系数的简单正弦和余弦函数的和。图像信号也是一种信号&#xff0c;只不过是二维离散信号&#xff0c;通过傅里叶变换对图像进行变换可以图像存空域转换为频域进行更多的处理。本文主要简要描述傅里叶变换…

Heap及其应用

目录 堆的相关知识 什么是堆&#xff1f; 堆的性质&#xff1a; 堆的实现&#xff1a; 堆的结构&#xff1a; &#xff08;一&#xff09;堆的插入 向上调整法&#xff1a; 寻找父节点 循环结束条件 代码&#xff1a; &#xff08;二&#xff09;堆的删除 删除根节点…

Huggingface:免费开源AI人工智能API工具平台

| 【产品介绍】 • 名称 Huggingface • 成立/上线时间 2016年 • 具体描述 HuggingFace是一个开源的自然语言处理AI工具平台&#xff0c;它为NLP的开发者和研究者提供了一个简单、快速、高效、可靠的解决方案&#xff0c;让NLP变得更加简…

R绘制箱线图

代码大部分来自boxplot()函数的帮助文件&#xff0c;可以通过阅读帮助文件&#xff0c;调整代码中相应参数看下效果&#xff0c;进而可以理解相应的作用&#xff0c;帮助快速掌握barplot()函数的用法。 语法 Usage(来自帮助文件) barplot(height, ...)## Default S3 method: …

lua环境搭建数据类型

lua作为一门计算机语言&#xff0c;从语法角度个人感觉还是挺简洁的接下来我们从0开始学习lua语言。 1.首先我们需要下载lua开发工具包 在这里我们使用的工具是luadist 下载链接为&#xff1a;https://luadist.org/repository/下载后的压缩包解压后就能用。 2.接下来就是老生…

听GPT 讲Istio源代码--istioctl

在 Istio 项目的 istioctl 目录中&#xff0c;有一些子目录&#xff0c;每个目录都有不同的作用和功能。以下是这些子目录的详细介绍&#xff1a; /pkg: pkg 目录包含了 istioctl 工具的核心代码和库。这些代码和库提供了与 Istio 控制平面交互的功能&#xff0c;例如获取和修改…

postgresql 内核源码分析 btree索引插入分析,索引页面分裂流程,多举措进行并发优化,对异常进行保护处理

Btree索引插入流程分析 ​专栏内容&#xff1a; postgresql内核源码分析手写数据库toadb并发编程 ​开源贡献&#xff1a; toadb开源库 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&a…

Swing程序设计详解(一)

【今日】 “若你决定灿烂&#xff0c;山无遮&#xff0c;海无拦” 目录 初识Swing 一 Swing简述 二 Swing常用窗体 2.1 JFrame窗体 2.2 JDialog对话框 2.3JOptionPane小型对话框 (1)通知框 (2)确认框 (3)输入框 (4)自定义对话框 三 常用布局管理器 3.1 绝…

JWT生成与解析/JWT令牌前端存储

第一步&#xff1a;创建项目 添加Maven依赖&#xff1a; <dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.62</version> </dependency> <dependency><groupId>org.s…

【C++】深拷贝和浅拷贝 ② ( 默认拷贝构造函数是浅拷贝 | 代码示例 - 浅拷贝造成的问题 )

文章目录 一、默认拷贝构造函数是浅拷贝1、默认拷贝构造函数2、默认拷贝构造函数是浅拷贝机制 二、代码示例 - 浅拷贝造成的问题 一、默认拷贝构造函数是浅拷贝 1、默认拷贝构造函数 如果 C 类中 没有定义拷贝构造函数 , C 编译器会自动为该类提供一个 " 默认的拷贝构造函…

GeoJSON转STL:地形3D打印

我们通过将 GeoJSON 形状坐标提取到点云中并使用 Open3d 应用泊松重建&#xff0c;从 GeoJSON 数据重建 STL 网格。 推荐&#xff1a;用 NSDT编辑器 快速搭建可编程3D场景 我对打印 GeoJSON 山丘的第一次尝试深感不满&#xff0c;因此想出了一个三步流程&#xff0c;仅使用开源…

Acwing 828. 模拟栈

Acwing 828. 模拟栈 题目要求思路讲解代码展示 题目要求 思路讲解 栈&#xff1a;先进后出 队列&#xff1a;先进先出 代码展示 #include <iostream>using namespace std;const int N 100010;int m; int stk[N], tt;int main() {cin >> m;while (m -- ){string o…

【JVM】经典垃圾收集器

文章目录 说明新生代收集器Serial收集器ParNew收集器Parallel Scavenge收集器 老年代收集器Serial Old收集器Parallel Old收集器CMS收集器 Garbage First收集器需要解决的问题运作过程CMS和G1的区别 说明 Java中有许多垃圾收集器&#xff08;Garbage Collector&#xff0c;GC&…

Spring Cloud Alibaba系列之nacos:(5)源码本地环境搭建

传送门 Spring Cloud Alibaba系列之nacos&#xff1a;(1)安装 Spring Cloud Alibaba系列之nacos&#xff1a;(2)单机模式支持mysql Spring Cloud Alibaba系列之nacos&#xff1a;(3)服务注册发现 Spring Cloud Alibaba系列之nacos&#xff1a;(4)配置管理 为什么要搭建本地…

范文展示,如何三步写出一篇满意的论文

第一步&#xff1a;输入文章关键信息 文章标题&#xff0c;写论文的话即为拟定的论文标题&#xff0c;例如这篇范文中的题目为“阳明心学研究” 关键词&#xff0c;可以写出多个论文主题相关的关键词&#xff0c;用逗号分开&#xff0c;例如这篇范文中只写了一个关键词“王阳…

CentOS 7.6使用mysql-8.0.31-1.el7.x86_64.rpm-bundle.tar安装Mysql 8.0

https://downloads.mysql.com/archives/community/是社区版的官网&#xff0c;可以选择版本下载。 cat /etc/redhat-release可以看到系统版本是CentOS Linux release 7.6.1810 (Core)&#xff0c;uname -r可以看到版本是3.10.0-957.el7.x86_64。 yum remove -y mysql-libs把…

计算机硬件基本组成和各硬件工作原理

计算机硬件基本组成和各硬件工作原理 计算机硬件基本组成早期冯若依曼机的结构冯若依曼机的特点 现代计算机的结构思维导图 各硬件工作原理主存储器运算器控制器I/O 计算机硬件基本组成 计算机硬件基本组成可分两大类 1.早期冯若依曼机的结构 2.现代计算机的结构 早期冯若依曼机…

类与对象的创建

package com.mypackage.oop.later;//学生类 //类里面只存在属性和方法 public class Student {//属性&#xff1a;字段//在类里面方法外面定义一个属性&#xff08;或者说是变量&#xff09;&#xff0c;然后在方法里面对他进行不同的实例化String name; //会有一个默认值&…

在word文档中找不到endnote的选项卡

本人由于在下载endnote之后才下载的office&#xff0c;所以导致在word文档中找不到endnote的选项卡&#xff0c;自己摸索到了解决方法。 首先确保已经拥有word与endnote之后&#xff0c;右键endnote打开所在文件夹&#xff1a; 在文件夹中找到这个Configure endnote.exe运行 之…