Java Multiple Thread

228 阅读26分钟

1.What's Thread & Process & their relationship ?

何为进程?

进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。进程是系统进行资源分配的最小单位.

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

进程的调度 算法

  • 先到先服务(FCFS)调度算法 : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
  • 短作业优先(SJF)的调度算法 : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。

非抢占式的调度算法,按估计运行时间最短的顺序进行调度。

长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。

  • 最短剩余时间优先 shortest remaining time next(SRTN) 最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
  • 高响应比优先调度算法(HRRN,Highest Response Ratio Next) 按照高响应比((已等待时间+要求运行时间)/ 要求运行时间)优先的原则【等待时间长和运行时间短都会增加其优先值】,每次先计算就绪队列中每个进程的响应比,然后选择其值最大的进程投入运行。
  • 时间片轮转调度算法 : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
  • 优先级调度 : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。
  • 多级反馈队列调度算法 :前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。

一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。

多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。

每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。

可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。

何为线程?

线程是一个比进程更小的执行单位,是CPU调度和分派的基本单位,。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

Java 程序天生就是多线程程序

总结: 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反

区别

一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和本地方法栈

  • 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
  • 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
  • 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
  • 通信方面:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。

2. Thread's Status in Java

  1. NEW

Thread state for a thread which has not yet started.

尚未启动的线程的线程状态。没有运行,只是创建了。

  1. RUNNABLE

Thread state for a thread which has not yet started.Thread state for a runnable thread. A thread in the runnable state is executing in the Java virtual machine but it maybe waiting for other resources from the operating system such as processor.

已经启动,正等待着第一次给予资源的运行

  1. BLOCKED

Thread state for a thread blocked waiting for a monitor lock.

  • A thread in the blocked state is waiting for a monitor lock

  • to enter a synchronized block/method or

  • reenter a synchronized block/method after calling

等待一个被锁定的线程,如synchronized的线程同步,或被阻塞的线程

  1. WAITING


 * Thread state for a waiting thread.

 * A thread is in the waiting state due to calling one of the

 * following methods:

 * <ul>

 *   <li>{  @link Object#wait() Object.wait} with no timeout</li>

 *   <li>{  @link #join() Thread.join} with no timeout</li>

 *   <li>{  @link LockSupport#park() LockSupport.park}</li>

 * </ul>

 *

 * <p>A thread in the waiting state is waiting for another thread to

 * perform a particular action.

 *

 * For example, a thread that has called {  @code Object.wait()}

 * on an object is waiting for another thread to call

 * {  @code Object.notify()} or {  @code Object.notifyAll()} on

 * that object. A thread that has called {  @code Thread.join()}

 * is waiting for a specified thread to terminate.

线程处于等待状态,如调用wait方法,或者其他线程的加入等引起的等待

  1. TIMED_WAITING
 /**

         * Thread state for a waiting thread with a specified waiting time.

         * A thread is in the timed waiting state due to calling one of

         * the following methods with a specified positive waiting time:

         * <ul>

         *   <li>{@link #sleep Thread.sleep}</li>

         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>

         *   <li>{@link #join(long) Thread.join} with timeout</li>

         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>

         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>

         * </ul>

         */

定时等待,如sleep方法,wait和join的超时

6.TERMINATED

  • The thread has completed execution.
  • Thread state for a terminated thread.

线程被终止或者线程被完成执行

3. Parallelism & concurrency

Erlang 之父 Joe Armstrong 通过图解释了并发与并行的区别

并发就是两个队列交替使用咖啡机,并行是两个队列同时使用两台咖啡机。这里的关键在于 同时。 对于进程来说,并发是快速交替使用处理机,在宏观上感觉是“同时”的,但实际是交替的。而并行是真正的同时在多个处理机上同时运行。

4. What's deadlock & conditions that can make deadlock

死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,如果没有外界干预,永远无法前往执行。

当且仅当以下所有条件在系统中同时成立时,才会出现资源上的死锁:

  1. 互斥条件:必须以非共享模式持有至少一个资源。否则,在必要时无法阻止进程使用资源。在任何给定的时刻,只有一个进程可以使用该资源。
  2. 请求和保持条件:进程当前持有至少一个资源并请求其他进程占有的额外资源。
  3. 不可抢占条件:资源只能由占有它的进程自动释放。
  4. 循环等待条件:每个进程必须等待由另一个进程占有的资源,相应地,另一个进程又在等待第一个进程释放资源。一般来说,有一组等待进程的集合 P = {P1, P2, …, PN},其中P1等待P2占有的资源,P2等待P3占有的资源,以此类推,直到PN等待P1持有的资源。

产生死锁的原因主要是:

  1. 竞争不可抢占资源
  2. 进程运行推进的顺序不合适。

处理死锁的基本方法

  1. 预防死锁:通过设置一些限制条件,去破坏一个或多个死锁的必要条件。
  2. 避免死锁:在资源分配的过程中,使用某种发放避免系统进入不安全的状态,从而避免死锁
  3. 检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉
  4. 解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。

5. Methods that processes communicate(from linux aspect)

1.进程间通信的概念

每个进程各自有不同的用户地址空间,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)

一.间接通信

1.1管道/匿名管道(pipe)

  • 管道是半双工的,即某一时刻只能单向传输,要实现双方进程互动,则需要定义两个管道。
  • 单独构成一种独立的文件系统,管道对于管道两端的进程而言,就是一个文件。
  • 管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。
  • 从管道读数据是一次性的操作,数据一旦被读取,就被管道所抛弃,释放空间。
  • 作用于具有血缘关系的进程之间。
  • 在读写时需要确定对方的存在,否则将退出。如果管道发现另一端断开,将自动退出。

1.2有名管道(FIFO)

  • 为了克服匿名管道,只能用于亲缘关系的进程间通信的缺点,提出了有名管道(FIFO)。
  • 有名管道提供一个路径名与之关联,以有名管道的文件形式存在于文件系统中,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信。
  • 有名管道的名字存在于文件系统中,内容存放在内存中。
  • 有名管道在打开时需要确实对方的存在,否则将阻塞。即以读方式打开某管道,在此之前必须一个进程以写方式打开管道。可以以读写(O_RDWR)模式打开有名管道,即当前进程读,当前进程写。

总结:

所谓管道通信,是指在进程之间通过一个共享文件,连接一个读进程和一个写进程以实现他们之间的半双工通信。

2.消息(Message)队列

  • 消息队列是存放在内核中的消息链表,具有特定的格式,每个消息队列由消息队列标识符表示。
  • 与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统和内存)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
  • 另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。(不需要双方同时在场)
  • 消息队列允许一个或多个进程向它写入与读取消息.
  • 管道和消息队列的通信数据都是先进先出的原则。
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
  • 消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
  • 目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。

总结:

在不需要收发消息双方都在的情况下,通过对内核中的消息链表的读写来进行进程通信。

3. 信号(Signal)

  • 信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
  • 如果该进程当前并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给它为止。
  • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

信号来源

信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:

  • 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。
  • 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。

信号生命周期和处理流程

  • 信号被某个进程产生(内核进程,因为中断等在内核中处理),并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统;
  • 操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号
  • 目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。

总结:

进程通过操作系统将产生的信号传递给指定进程,指定进程根据设置的预处理方式,进行相应中断操作,进程也可以设置是否阻塞信号的接收。

4. 信号量(semaphore)

信号量是一个计数器,有初值(>0),当有进程申请使用信号量,通过一个P操作来对信号量进行-1操作,当计数器减到0的时候就说明没有资源了,其他进程要想访问就必须等待(比如忙等待或者睡眠),当该进程执行完这段工作(临界区)之后,就会执行V操作来对信号量进行+1操作。

信号量的使用

  1. 创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。
  2. 进程要进入临界区时,检测该资源的信号量;
  3. 若该资源的信号量值大于0,则进程可以使用该资源,进程信号量值减1。若该资源的信号量值为0,则进程进入休眠状态,直到信号量值大于0时进程被唤醒,访问该资源;(P操作)
  4. 当进程不再使用由一个信号量控制的共享资源时,该信号量值增加1(v操作),如果此时有进程处于休眠状态等待此信号量,则该进程会被唤醒。

临界区:临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。

临界资源:只能被一个进程同时使用(不可以多个进程共享),要用到互斥。

为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。Linux环境中,有三种类型:Posix(可移植性操作系统接口)有名信号量(使用Posix IPC名字标识)Posix基于内存的信号量(存放在共享内存区中)System V信号量(在内核中维护) 。这三种信号量都可用于进程间或线程间的同步。

两个进程使用一个二值信号量

两个进程所以用一个Posix有名二值信号量

一个进程两个线程共享基于内存的信号量

信号量与普通整型变量的区别:

(1)信号量是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap) , signal(semap) ; 来进行访问;

(2)操作也被成为PV原语(P表示通过的意思,V表示释放的意思),而普通整型变量则可以在任何语句块中被访问;

信号量与互斥量之间的区别:

(1)互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。

互斥: 是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步: 是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。

在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

(2)互斥量值只能为0/1,信号量值可以为非负整数。

也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量时,也可以完成一个资源的互斥访问。

(3)互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

总结:

信号量是:多线程通过P,V操作对设定的信号量的值进行增减操作,以达到线程之间对临界资源同步与互斥访问的目的。

5.套接字(socket)

套接字是一种通信机制,凭借这种机制,可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。

套接字有3种类型:

  • 流式套接字,即TCP套接字,用SOCK_STREAM表示
  • 数据报套接字,即UDP套接字(或称无连接套接字),用SOCK_DGRAM表示
  • 原始套接字,用SOCK_RAM表示

套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。

(1)套接字的域

它指定套接字通信中使用的网络介质,最常见的套接字域有两种:

是AF_INET,它指的是Internet网络 当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务,所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。

是AF_UNIX,表示UNIX文件系统 它就是文件输入/输出,而它的地址就是文件名。

(2)套接字的端口号

每一个基于TCP/IP网络通讯的程序(进程)都被赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留Socket中的输入/输出信息,端口号是一个16位无符号整数,范围是0-65535,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于256的端口号保留给标准应用程序,比如pop3的端口号就是110,每一个套接字都组合进了IP地址、端口,这样形成的整体就可以区别每一个套接字。

(3)套接字协议类型

因特网提供三种通信机制,

一是流套接字, 流套接字在域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。

二个是数据报套接字, 它不需要建立连接和维持一个连接,它们在域中通常是通过UDP/IP协议实现的。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,UDP不是一个可靠的协议,但是它的速度比较高,因为它并一需要总是要建立和维持一个连接。

三是原始套接字, 原始套接字允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。

原始套接字与标准套接字的区别在于:

原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

总结:

不同主机上的进程,通过网络借助不同的协议类型及其相应ip和端口号组成的套接字,来达到跨计算机通信的目的。

二.直接通信

1.共享内存(share memory)

  • 使得多个进程可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
  • 为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率
  • 由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。

总结:

多个进程可以把一段进程共用的内存映射到自己的进程空间中,从而实现数据的共享和传输,它是存在于内核级别的一种资源,是所有进程间通信中方式最快的一种。

6.线程通信

  • volatile关键字方式: volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。volatile语义保证线程可见性有两个原则保证

所有volatile修饰的变量一旦被某个线程更改,必须立即刷新到主内存

所有volatile修饰的变量在使用之前必须重新读取主内存的值

  • 等待/通知机制: 等待通知机制是基于wait和notify方法来实现的,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。
  • join方式: join其实合理理解成是线程合并,当在一个线程调用另一个线程的join方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行,所以join的好处能够保证线程的执行顺序,但是如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的,最后join的实现其实是基于等待通知机制的
  • threadLocal方式: threadLocal方式的线程通信,不像以上三种方式是多个线程之间的通信,它更像是一个线程内部的通信,将当前线程和一个map绑定,在当前线程内可以任意存取数据,减省了方法调用间参数的传递。

7.同步方式

进程同步方式

① 临界区: 对临界资源进行访问的那段代码称为临界区。

为了互斥访问临界资源,每个进程在进入临界区之前,需要先进行检查。

② 同步与互斥

同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。

互斥:多个进程在同一时刻只有一个进程能进入临界区。

③ 信号量: 信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。

  • down : 如果信号量大于 0 ,执行 -1 操作;信号量等于 0,进程睡眠,等待信号量大于 0;
  • up :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 down 操作。

down 和 up 操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。

如果信号量的取值只能为 0 或者 1,那么就成为了 互斥量(Mutex) ,0 表示临界区已经加锁,1 表示临界区解锁。

④ 管程: 使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。

线程同步的方式

临界区用于单个进程中线程间的同步;

互斥量、信号量、事件用于多个进程间的各个线程间实现同步

  • 临界区: 使用临界区对象。拥有临界区对象的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区对象的线程放弃临界区对象为止【只用于同一进程】
  • 互斥量: 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以可以保证公共资源不会同时被多个线程访问。【互斥对象和临界区对象非常相似,只是其允许在进程间使用,而临界区只限制于同一进程的各个线程之间使用】
  • 信号量: 允许多个线程同一时刻访问同一资源,但是需要限制同一时刻访问此资源的最大线程数目。

信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值 S 与相应资源的使用情况有关。当 S 大于 0 时,表示当前可用资源的数量;当 S 小于 0 时,其绝对值表示等待使用该资源的进程个数。注意,信号量的值仅能由 PV 操作来改变。

执行一次 P 操作意味着请求分配一个单位资源,因此S的值减1;当 S < 0 时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。

而执行一个 V 操作意味着释放一个单位资源,因此 S 的值加 1;若 S < 0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。

  • 事件(信号): 事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。【进程间通信中唯一的一个异步机制】

8.线程池