1.管道(匿名管道 Pipe)// PipedInputStream 、PipedOutputStream
2.命名管道(NamedPipe/FIFO)//java 不支持?
3.信号(Signal) // wait() notify() notifyall() 、管程
4.消息队列(MessageQueue)
5.共享内存 (SharedMemory) //java 不支持?
6.内存映射(MappedMemory)
7.信号量(Semaphore)//理解其原子性操作,也是信号量之所以安全的原因
8.套接字(Socket) //应该都熟悉
管道(Pipe/匿名管道):在Linux下“一切皆文件”,其实这里的管道就是一个文件。管道实现进程通信就是让两个进程都能访问该文件。 管道流是JAVA中线程通讯的常用方式之一,
管道的特征:
①单向的,也就是说,两个进程都能访问这个文件,假设进程1往文件内写东西,那么进程2 就只能读取文件的内容。 简而言之数据只能从一端流向另一端而不应该双向流动否则会造成混乱。如果要两个线程之间互通讯,则需要两个管道流。
②只能用于具有血缘关系的进程间通信,通常用于父子进程建通信
③管道是基于字节流来通信的 //byte类型
④依赖于文件系统,它的生命周期随进程的结束结束
⑤其本身自带同步互斥效果,半双工通信
管道容量大小:64K
使用管道需要注意的4种特殊情况:
(1)如果所有指向管道写端的文件描述符都关闭了,而仍然有进程从管道的读端读数据,那么文件内的所有内容被读完后再次read就会返回0,就像读到文件结尾。//强读返回0
(2)如果有指向管道写端的文件描述符没有关闭(管道写段的引用计数大于0),而持有管道写端的进程没有向管道内写入数据,假如这时有进程从管道读端读数据,那么读完管道内剩余的数据后就会阻塞等待,直到有数据可读才读取数据并返回。//空读会阻塞
(3)如果所有指向管道读端的文件描述符都关闭,此时有进程通过写端文件描述符向管道内写数据时,则该进程就会收到SIGPIPE信号,并异常终止。//强写则发生异常
(4)如果有指向管道读端的文件描述符没有关闭(管道读端的引用计数大于0),而持有管道读端的进程没有从管道内读数据,假如此时有进程通过管道写段写数据,那么管道被写满后就会被阻塞,直到管道内有空位置后才写入数据并返回。//写满则阻塞
JAVA实现方式
管道流是JAVA中线程通讯的常用方式之一,基本流程如下:
1)创建管道输出流PipedOutputStream pos和管道输入流PipedInputStream pis
2)将pos和pis匹配,pos.connect(pis);
3)将pos赋给信息输入线程,pis赋给信息获取线程,就可以实现线程间的通讯了
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
public class testPipeConnection {
public static void main(String[] args) {
/**
* 创建管道输出流
*/
PipedOutputStream pos = new PipedOutputStream();
/**
* 创建管道输入流
*/
PipedInputStream pis = new PipedInputStream();
try {
/**
* 将管道输入流与输出流连接 此过程也可通过重载的构造函数来实现
*/
pos.connect(pis);
} catch (IOException e) {
e.printStackTrace();
}
/**
* 创建生产者线程
*/
Producer p = new Producer(pos);
/**
* 创建消费者线程
*/
Consumer1 c1 = new Consumer1(pis);
/**
* 启动线程
*/
p.start();
c1.start();
}
}
/**
* 生产者线程(与一个管道输入流相关联)
*
*/
class Producer extends Thread {
private PipedOutputStream pos;
public Producer(PipedOutputStream pos) {
this.pos = pos;
}
public void run() {
int i = 0;
try {
while(true)
{
this.sleep(3000);
pos.write(i);
i++;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 消费者线程(与一个管道输入流相关联)
*
*/
class Consumer1 extends Thread {
private PipedInputStream pis;
public Consumer1(PipedInputStream pis) {
this.pis = pis;
}
public void run() {
try {
while(true)
{
System.out.println("consumer1:"+pis.read());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java中的管道只先于线程间通信,如果要跨进程,就要使用JNI了。
命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
Linux下消息队列用于运行于同一台机器上的进程间通信,它和管道很相似,是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现。消息链表中节点的结构用msg声明。事实上,它是一种正逐渐被淘汰的通信方式,我们可以用流管道或者套接口的方式来取代它,所以,我们对此方式也不再解释,也建议读者忽略这种方式。
消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
消息队列的主要特点是异步处理,主要目的是减少请求响应时间和解耦。所以主要的使用场景就是将比较耗时而且不需要即时(同步)返回结果的操作作为消息放入消息队列。同时由于使用了消息队列,只要保证消息格式不变,消息的发送方和接收方并不需要彼此联系,也不需要受对方的影响,即解耦和。
别人的java实现 https://www.cnblogs.com/pro-simian/p/7209806.html
在android系统中每个线程都有一个消息队列 Looper ,activity线程默认是开启消息循环机制的,子线程需要手动开启。
https://www.cnblogs.com/swbzmx/p/5992247.html
共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式,因为数据不需要在不同的进程间复制。是针对其他通信机制运行效率较低而设计的。通常由一个进程创建一块共享内存区,其余进程对这块内存区进行 读写。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
内存映射:内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
linux得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。
java应用中只能创建映像文件共享内存(内存映射)。
共享内存的使用有如下几个特点:
(1)可以被多个进程打开访问;
(2)读写操作的进程在执行读写操作时其他进程不能进行写操作;
(3)多个进程可以交替对某一共享内存执行写操作;
(4)一个进程执行了内存的写操作后,不影响其他进程对该内存的访问。同时其他进程对更新后的内存具有可见性。
(5)在进程执行写操作时如果异常退出,对其他进程写操作禁止应自动解除。
内存映射在java中的实现
jdk1.4中提供的类MappedByteBuffer为我们实现共享内存提供了较好的方法。该缓冲区实际上是一个磁盘文件的内存映像。二者的变化将保持同步,即内存数据发生变化会立刻反映到磁盘文件中,这样会有效的保证共享内存的实现。
将共享内存和磁盘文件建立联系的是文件通道类:FileChannel。该类的加入是JDK为了统一对外部设备(文件、网络接口等)的访问方法,并且加强了多线程对同一文件进行存取的安全性。例如读写操作统一成read和write。这里只是用它来建立共享内存用,它建立了共享内存和磁盘文件之间的一个通道。
打开一个文件建立一个文件通道可以用RandomAccessFile类中的方法getChannel。该方法将直接返回一个文件通道。该文件通道由于对应的文件设为随机存取文件,一方面可以进行读写两种操作,另一方面使用它不会破坏映像文件的内容(如果用FileOutputStream直接打开一个映像文件会将该文件的大小置为0,当然数据会全部丢失)。这里,如果用 FileOutputStream和FileInputStream则不能理想的实现共享内存的要求,因为这两个类同时实现自由的读写操作要困难得多。
// 获得一个只读的随机存取文件对象
RandomAccessFile RAFile = new RandomAccessFile(filename,"r");
// 获得相应的文件通道
FileChannel fc = RAFile.getChannel();
// 取得文件的实际大小,以便映像到共享内存
int size = (int)fc.size();
// 获得共享内存缓冲区,该共享内存只读
MappedByteBuffer mapBuf = fc.map(FileChannel.MAP_RO,0,size);
// 获得一个可读写的随机存取文件对象
RAFile = new RandomAccessFile(filename,"rw");
// 获得相应的文件通道
fc = RAFile.getChannel();
// 取得文件的实际大小,以便映像到共享内存
size = (int)fc.size();
// 获得共享内存缓冲区,该共享内存可读写
mapBuf = fc.map(FileChannel.MAP_RW,0,size);
// 获取头部消息:存取权限
mode = mapBuf.getInt();
对共享内存读写时应该考虑同步的问题
共享内存在java应用中,经常有如下两种种应用:
(1)永久对象配置
在java服务器应用中,用户可能会在运行过程中配置一些参数,而这些参数需要永久有效,当服务器应用重新启动后,这些配置参数仍然可以对应用起作用。这就可以用到该文中的共享内存。该共享内存中保存了服务器的运行参数和一些对象运行特性。可以在应用启动时读入以启用以前配置的参数。
(2)查询共享数据
一个应用(例 sys.java)是系统的服务进程,其系统的运行状态记录在共享内存中,其中运行状态可能是不断变化的。为了随时了解系统的运行状态,启动另一个应用(例 mon.java),该应用查询该共享内存,汇报系统的运行状态。
可见,共享内存在java应用中还是很有用的,只要组织好共享内存的数据结构,共享内存就可以在应用开发中发挥很不错的作用。
参考:信号和信号量的比较 以及在java中的实现
https://blog.csdn.net/sunset108/article/details/38819529
信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送 信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
信号(Signal)是一种处理异步事件的通讯方式,用于通知其他进程或者自己本身,来告知将有某种事件发生。在Java中,信号机制通过wait(),notify()和notifyAll()来实现。其中wait()使得当前调用wait()的线程挂起,并释放已经获得的wait()所在代码块的锁;notify()用于随即唤醒一个被wait()挂起的线程进入线程调度队列;notifyAll()用于唤醒所有被wait()挂起的线程进入线程调度队列。
(java 实现管程时可以使用wait 和notify实现)
Semaphore是用来保护一个或者多个共享资源的访问,Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。
如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
主要作为进程间以及同一进程不同线程之间的同步手段。
Semaphore 的原子操作:检查数值、修改变量值、以及可能发生的睡眠操作均作为一个单一的不可分割的原子操作。
java实现
Semaphore semaphore = new Semaphore(10,true);
semaphore.acquire();
//do something here
semaphore.release();