多线程面试题汇总(一)

本文是作者整理的个人笔记,文中可能引用到其他人的成果但是未指明出处,如有不妥,请指正,谢谢!

转载注明:

概念题

1、线程的基本概念、线程的基本状态与状态之间的关系?

线程是进程里面一个执行上下文,或者是执行序列。线程是进程级别上的多道编程。同一进程的多个线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行时的堆栈。线程之间切换快,无需陷入内核态。

线程状态

阻塞状态分分为三种:等待阻塞(wait)、同步阻塞( 加锁 )、其他阻塞(睡眠)

新建状态

当用new操作符创建一个线程时。此时程序还没有开始运行线程中的代码。

就绪状态

一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序来调度的。

运行状态(runing)

当线程获得 CPU时间后,它才进图运行状态,真正开始执行run()方法。

阻塞状态(blocked)

线程运行过程中,可能由于各种原因进入阻塞状态:

①线程通过调用sleep方法进入睡眠状态;

②线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;

③线程试图得到一个锁,而该锁正被其他线程持有;

④线程在等待某个触发条件;

所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。

死亡状态(dead)

有两个原因会导致线程死亡:

①run方法正常退出而自然死亡;

②一个未捕获的异常终止了run方法而使线程猝死;

为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法,如果是可运行或被阻塞,这个方法返回true;如果线程仍旧是new状态且不是可运行的,或者线程死亡了,则返回false。

2、进程与线程的区别:

一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其他线程,多个线程并发地运行在同一个进程。一个进程汇总的所有线程都在该进程的虚拟地址空间汇总,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。

基于进程的多任务处理是程序的并发执行。基于线程的多任务处理是同一程序的片段的并发执行;

地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;

资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的;

一个进程奔溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃则整个进程都死掉,所以多进程要比多线程健壮;

进程切换时,消耗资源大,效率高,所以涉及到频繁切换时,使用线程要好于进程;

执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存于应用程序中,有应用程序提供多个线程执行控制。

线程是处理器调度的基本单位,但进程不是。

3、多线程有几种实现方法

C++创建多线程的三种方式:

1.CreateThread()

CreateThread()是windows的API函数,提供操作系统级别的创建线程的操作,且仅限于工作者线程。不调用MFC和RTL函数(标准C语言函数),即只调用win32API时,可以 用CreateThread。在使用过程中要考虑到进程捡的同步与互斥关系(防止死锁)。

线程函数定义为: DWORD WINAPI _yourThreradFun(LPVOID)

头文件 <windows.h>

创建线程API:

 1 HANDLE CreateThread(
 2     __in   SEC_ATTRS
 3     SecurityAttributes,    //线程安全属性
 4     __in   ULONG
 5     StackSize,        // 堆栈大小 
 6     __in   SEC_THREAD_START
 7     StartFunction,    // 线程函数
 8     __in   PVOID
 9     ThreadParameter,  // 线程参数
10     __in   ULONG
11     CreationFlags,    // 线程创建属性
12     __out  PULONG
13     ThreadId          // 线程ID
14     );

复制代码

使用步骤:

HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
CloseHandle(hThread);

2、互斥API:互斥量实现

 1 HANDLE CreateMutex(
 2     LPSECURITY_ATTRIBUTES lpMutexAttributes,
 3     BOOL bInitialOwner,                       // 指定该资源初始是否归属创建它的进程
 4     LPCTSTR lpName                            // 指定资源的名称      
 5     );
 6 
 7 BOOL ReleaseMutex(
 8     HANDLE hMutex   // 该函数用于释放一个独占资源,进程一旦释放该资源,该资源就不再属于它了
 9     );
10 
11 DWORD WaitForSingleObject(
12     HANDLE hHandle,        // 指定所申请的资源的句柄
13     DWORD dwMilliseconds   // 一般指定为INFINITE,表示如果没有申请到资源就一直等待该资源
14     );

复制代码

3、互斥API:临界区实现

1 CRITICAL_SECTION cs;
2 InitializeCriticalSection(&cs);
3 EnterCriticalSection(&cs);
4 LeaveCriticalSection(&cs);
5 DeleteCriticalSection(&cs);

例子:

 1 #include <iostream>   
 2 #include <windows.h>   
 3 using namespace std;
 4 
 5 HANDLE hMutex;
 6 
 7 DWORD WINAPI Fun(LPVOID lpParamter)
 8 {
 9     while (1) {
10         WaitForSingleObject(hMutex, INFINITE);
11         cout << "Fun display!" << endl;
12         Sleep(1000);
13         ReleaseMutex(hMutex);
14     }
15 }
16 
17 int main()
18 {
19     HANDLE hThread = CreateThread(NULL, 0, Fun, NULL, 0, NULL);
20     hMutex = CreateMutex(NULL, FALSE, "screen");
21     CloseHandle(hThread);
22     while (1) {
23         WaitForSingleObject(hMutex, INFINITE);
24         cout << "main display!" << endl;
25         Sleep(2000);
26         ReleaseMutex(hMutex);
27     }
28 
29     return 0;
30 }

4、多线程同步和互斥实现方式

当有多个线程的时候,经常需要去同步这些线程以访问同一个数据或资源。例如,假设有一个程序,其中一个线程用于把文件读到内存,而另一个线程用于统计文件中的字符数。当然,在把整个文件调入内存之前,统计它的计数是没有意义的。但是,由于每个操作都有自己的线程,操作系统会把两个线程当作是互不相干的任务分别执行,这样就可能在没有把整个文件装入内存时统计字数。为解决此问题,你必须使两个线程同步工作。

所谓同步,是指在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。如果用对资源的访问来定义的话,同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

所谓互斥,是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行它们之中的任一程序片段,只能等到该进程运行完这个程序片段后才可以运行。如果用对资源的访问来定义的话,互斥某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

实现方式:

线程间的同步方法大体可分为两类:用户模式和内核模式。内核模式就是指利用系统内核对象的单一性来进行同步,使用时需要切换内核态与用户态,而用户模式就是不需要切换到内核态,只在用户态完成操作。

用户模式下的方法有:原子操作(例如一个单一的全局变量),临界区。

内核模式下的方法有:事件,信号量,互斥量。

1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问(可能造成竞争的共享资源)。 
2、互斥量:为协调共同对一个共享资源的单独访问而设计的。 
3、信号量:为控制一个具有有限数量用户资源而设计。 
4、事 件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

同步的目的:多线程程序 的执行结果有可能是不确定的---》为了消除不确定,产生了线程同步,即就是不管线程之间的执行如何穿梭,其运行结果都是正确的。

5、多线程同步和互斥有何异同,在什么情况下分别使用它们?

线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。

线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步(下文统称为同步)。

线程同步解决多线程并发问题:

几种内核对象中,除了互斥量,没有任何一个会记住自己是哪个线程等待成功的。

互斥量和关键段的比较:

(1)关键段只适用于同一个进程,互斥量可适用与不同的进程

(2)关键段的效率更高

(3)关键段不安全,而互斥量更安全;

假如把整条道路看成是一个【进程】的话,那么马路中间白色虚线分隔开来的各个车道就是进程中的各个【线程】了。
  ①这些线程(车道)共享了进程(道路)的公共资源(土地资源)。
  ②这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。
  ③这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。
  ④这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。
  ⑤这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道。
  注:
  由于用于互斥的信号量sem与所有的并发进程有关,所以称之为公有信号量。公有信号量的值反映了公有资源的数量。只要把临界区置于P(sem)和V(sem)之间,即可实现进程间的互斥。就象火车中的每节车厢只有一个卫生间,该车厢的所有旅客共享这个公有资源:卫生间,所以旅客间必须互斥进入卫生间,只要把卫生间放在P(sem)和V(sem)之间,就可以到达互斥的效果。

猜你喜欢

转载自blog.csdn.net/weixin_35802502/article/details/81143482