进程与线程是经常一起出现的概念,面试过程中也经常被问到
一、基本概念
1、进程
进程是具有一定独立功能的程序、它是系统进行资源分配和调度的一个独立单位,重点在系统调度和单独的单位,也就是说进程是可以独立运行的一段程序。
2、线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源。
3、进程与线程的关系
从Java的角度讲,一个虚拟机就是一个进程,但是里面可以包含很多的线程。
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程(通常说的主线程)。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
- 线程是指进程内的一个执行单元,也是进程内的可调度实体。
换个角度来理解他们之间的区别:
- 调度:线程是CPU调度和分配的基本单位,进程是拥有资源的基本单位。
- 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
二、线程间通信方法
关于线程间通信方式有很多种说法,我觉得比较合理的是
- wait、notify机制
- await、signal机制
- 管道
1、wait、notify
这两个方法的使用已经分析过,这两个方法的关键就是一个对象的锁和某个条件。
- 多个线程之间通过持有同一个对象锁来形成某种联系,并且同时只有一个线程能够持有锁
- 当一个线程持有锁时,检测是否满足某个条件,如果不满足,那么就释放这个锁,开始等待
- 如果这个条件满足了,就执行自己的操作并且在执行完成之后再释放锁并且通知其他正在等待的线程
这样通过notify方法来实现对特定线程之间的通信。
2、await、signal
这两个方法也介绍过,他们是Thread类中的方法,是上面两个的升级版。
上面说到的对象锁,对象锁的作用就是是把所有持有这个锁的线程划到同一个集合,使这些线程之间可以相互通信。但是这样有一个弊端就是一旦通知就会通知该集合内的所有线程,而显示锁的这个机制则可以通过Condition把这个集合再划分为n个小集合,这样所有的线程都持有这个显示锁,但是通知的时候却可以通知这个集合内某个小集合内的线程。
以生产者消费者模型为例子,
生产者线程和消费者线程都需要操作一个共同的队列,那么这个队列就是他们共同要持有的对象锁,
wait机制通过持有这个队列的对象锁
await机制通过一个显示的Lock
都是保证同时只能有一个线程来访问这个队列
当一个先成为完成工作后,比如消费者发现队列已空,他就会通知其他线程
- wait的notify方法会通知所有的线程,无法区分是生产者还是消费者
- 而await的signal却可以通过定义Lock方法的两个Condition,而生产者线程和消费者线程等待的是不同的Condition对象,这样就可以实现更精确的通信,比如队列空的时候只要通知生产者就可以了,通知消费者是没有意义的
3、管道
管道流是JAVA中线程通讯的常用方式之一,基本流程如下:
- 创建管道输出流PipedOutputStream pos和管道输入流PipedInputStream pis
- 将pos和pis匹配,pos.connect(pis);
- 将pos赋给信息输入线程,pis赋给信息获取线程,就可以实现线程间的通讯了
管道流虽然使用起来方便,但是也有一些缺点
- 管道流只能在两个线程之间传递数据
- 管道流只能实现单向发送,如果要两个线程之间互通讯,则需要两个管道流
三、进程间通信方式
进程间的通信方式属于操作系统的范畴:
- 管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。
- 命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关 系 进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
- 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送 信号给进程本身;Linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
- 消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
- 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
- 内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
- 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
- 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
1、管道
它传递数据是单向性的,只能从一方流向另一方,也就是一种半双工的通信方式;只用于有亲缘关系的进程间的通信,亲缘关系也就是父子进程或兄弟进程;没有名字并且大小受限,传输的是无格式的流,所以两进程通信时必须约定好数据通信的格式。管道它就像一个特殊的文件,但这个文件之存在于内存中,在创建管道时,系统为管道分配了一个页面作为数据缓冲区,进程对这个数据缓冲区进行读写,以此来完成通信。其中一个进程只能读一个只能写,所以叫半双工通信,为什么一个只能读一个只能写呢?因为写进程是在缓冲区的末尾写入,读进程是在缓冲区的头部读取,他们各自的数据结构不同,所以功能不同。
2、有名管道
看见这个名字就能知道个大概了,它于管道的不同的是它有名字了。这就不同与管道只能在具有亲缘关系的进程间通信了。它提供了一个路径名与之关联,有了自己的传输格式。有名管道和管道的不同之处还有一点是,有名管道是个设备文件,存储在文件系统中,没有亲缘关系的进程也可以访问,但是它要按照先进先出的原则读取数据。同样也是单双工的。
3、信号
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源。信号分为可靠信号和不可靠信号,实时信号和非实时信号。进程有三种方式响应信号1.忽略信号2.捕捉信号3.执行缺省操作。
4、消息队列
存放在内核中的消息链表,每个消息队列由消息队列标识符标识,于管道不同的是,消息队列存放在内核中,只有在内核重启时才能删除一个消息队列,内核重启也就是系统重启,同样消息队列的大小也是受限制的。
5、共享内存
就是分配一块能被其他进程访问的内存。共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。首先说下在使用共享内存区前,必须通过系统函数将其附加到进程的地址空间或说为映射到进程空间。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到 进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互 斥锁和信号量都可以。采用共享内存通信的一个显而易 见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而 共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就 解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存 中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
6、内存映射
在Java NIO中接触过,不知道是不是
7、信号量
也可以说是一个计数器,常用来处理进程或线程同步的问题,特别是对临界资源的访问同步问题。临界资源:为某一时刻只能由一个进程或线程操作的资源,当信号量的值大于或等于0时,表示可以供并发进程访问的临界资源数,当小于0时,表示正在等待使用临界资源的进程数。更重要的是,信号量的值仅能由PV操作来改变。
8、套接字
Socket