Java通信方式总结
1 Java对象、方法间通信方式
1.1 值传递、引用传递
(1)值传递:将副本传递给方法,调用方法改变副本的值,但是并不改变原值
(2)引用传递:传递的是对象(或者变量)的引用,对其修改,会改变原值
从内存分配的角度讲解Java中只存在值传递,不存在引用传递
1.2 浅拷贝、深拷贝
(1)浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 例程图解: (2)深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 例程图解:
2 Java进程间通信方式
2.1 进程通信的目的
(1)数据传输:一个进程需要将它的数据发送给另一个进程。 (2)资源共享:多个进程之间共享同样的资源。 (3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件 (4)进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
2.2 进程间通信的方式
2.2.1 常用方式
(1)管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用,进程的亲缘关系通常是指父子进程关系。
(2) 消息队列(messagequeue) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识、消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
(3)信号(sinal): 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
(4)共享内存(shared memory) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
(5)内存映射(mapped memory)
(6)套接字(socket ): 套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
2.2.2 其他通信方式(远程通信)
(1)RPC(Remote Procedure Call,RPC,远程过程调用)
(2)Web service
(3)RMI(Remote Method Invocation,RMI,远程方法调用)
(4)JMS等
2.2.2.1 RPC
概念:远程过程调用(Remote Procedure Call,缩写为RPC),是一种用于构建基于C/S(客户端/服务器)的分布式应用程序技术。调用者与被调用者可能在同一台服务器上,也可能在由网络连接的不同服务器上,对于他们来说,网络通信是透明的,就是像调用本地方法一样调用远程方法。
简单来说:RPC(remote procedure call)是指远程过程调用,比如两台服务器A和B,A服务器上部署一个应用,B服务器上部署一个应用,A服务器上的应用想调用B服务器上的应用提供的接口,由于不在一个内存空间,不能直接调用,所以需要通过网络来表达调用的语义和传达调用的数据。RPC就是用来传输这些远程调用的语义和数据的框架。RPC只是一种编程模型而非一种规范或协议,并没有规定具体怎样实现。 关键步骤:
- 首先,要解决通讯的问题。主要是通过客户端和服务器端之间建立TCP连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享一个连接。
- 第二,要解决寻址的问题。A服务器上的应用要调用B服务器上的应用,A服务器上的应用需要通过底层RPC框架得知:如何连接到B服务器(主机或IP地址)以及特定的端口,方法的名称等信息,这样才能完成调用。(如函数对应表)
- 第三,发起调用需要将调用语义和数据进行序列化。A服务器上的应用发起远程调用时,方法的参数需要通过底层的网络协议如TCP传递到B服务器,由于网络协议是基于二进制的,内存中的参数需要序列化成二进制形式,然后再通过寻址和传输将序列化的二进制发送给B服务器。
- 第四:B服务器收到请求后,需要进行反序列化。恢复为内存中的表达方式,然后找到对应的方法进行本地调用并返回,序列化返回值并发送给A服务器。
- 第五:处理返回值。A服务器收到B服务器的返回值后,进行反序列化,恢复为内存中的表达方式,然后交给A服务器上的应用进行处理。
例程系统调用过程:
2.2.2.2 WebService
1、概念:WebService是一种跨编程语言和跨操作系统平台的远程调用技术。
2、基础:WebService提供的服务是基于web容器的,底层使用http协议,类似一个远程的服务提供者。比如天气预报服务,对各地客户端提供天气预报,是一种请求应答的机制,是跨系统跨平台的,就是通过一个servlet,提供服务给其他应用请求。
3、传输:WebService采用HTTP协议传输数据,采用XML格式封装数据(即XML中说明调用远程服务对象的哪个方法,传递的参数是什么,以及服务对象的返回结果是什么)。XML是WebService平台中表示数据的格式。除了易于建立和易于分析外,XML主要的优点在于它既是平台无关的,又是厂商无关的。无关性是比技术优越性更重要的:软件厂商是不会选择一个由竞争对手所发明的技术的。但是XML虽然解决了数据表达问题,却留下了数据格式类型问题,XSD(XML Schema)就是专门解决这个问题的一套标准。它定义了一套标准的数据类型,并给出了一种语言来扩展这套数据类型。
4、WebService三种基本元素:
1)SOAP
SOAP 基于XML 和 HTTP ,其通过XML 来实现消息描述,然后再通过 HTTP 实现消息传输。
Web Service通过HTTP协议发送请求和接收结果时,发送的请求内容和结果内容都采用XML格式封装,并增加了一些特定的HTTP消息头,以说明HTTP消息的内容格式,这些特定的HTTP消息头和XML内容格式就是SOAP协议。SOAP提供了标准RPC方法来调用Web Service。
SOAP协议 = HTTP协议 + XML数据格式
- SOAP 是XML Web Service 的通信协议。
- SOAP 是一种规范,用来定义消息的XML 格式 。包含在一对SOAP元素中的、结构正确的XML 段就是SOAP 消息。
2)WSDL(Web Services Description Language,对外的服务描述语言,就是广告)
类似于我们去商店买东西,首先要知道商店里有什么东西可买,然后再来购买,商家的做法就是张贴广告海报。
WebService也一样,WebService客户端要调用一个WebService服务,首先要知道这个服务的地址在哪,以及这个服务里有什么方法可以调用,所以,WebService务器端首先要通过一个WSDL文件来说明自己家里有啥服务可以对外调用,服务是什么(服务中有哪些方法,方法接受的参数是什么,返回值是什么),服务的网络地址用哪个url地址表示,服务通过什么方式来调用。
WSDL(Web Services Description Language)就是这样一个基于XML的语言,用于描述Web Service及其函数、参数和返回值。它是WebService客户端和服务器端都能理解的标准格式。因为是基于XML的,所以WSDL既是机器可阅读的,又是人可阅读的,这将是一个很大的好处。一些最新的开发工具既能根据你的Web service生成WSDL文档(生成广告),又能导入WSDL文档(阅读广告),生成调用相应WebService的代理类代码。
WSDL文件保存在Web服务器上,通过一个url地址就可以访问到它。客户端要调用一个WebService服务之前,要知道该服务的WSDL文件的地址。WebService服务提供商可以通过两种方式来暴露它的WSDL文件地址:
- 注册到UDDI服务器,以便被人查找
- 直接告诉给客户端调用者
3)UDDI(介绍)
UDDI 目录条目是介绍所提供的业务和服务的XML文件,UDDI 目录条目包括三个部分。
“白页”介绍提供服务的公司:名称、地址、联系方式等等;
“黄页”包括基于标准分类法的行业类别;
“绿页”详细介绍了访问服务的接口,以便用户能够编写应用程序以使用 Web 服务。
服务的定义是通过一个称为类型模型(或 tModel)的 UDDI文档来完成的。多数情况下,tModel包含一个WSDL 文件,用于说明访问 XMLWeb Service 的SOAP 接口,但是tModel非常灵活,可以说明几乎所有类型的服务。
2.2.2.3 RMI
Java RMI,一种用于实现远程过程调用(RPC)的Java API RMI交互图: 特点: 能直接传输序列化后的Java对象和分布式垃圾收集。它的实现依赖于Java虚拟机(JVM),因此它仅支持从一个JVM到另一个JVM的调用。甚至,它可以直接看成RPC的java版本。
总结起来,RMI的工作原理大致可以理解为:
1、服务器端提供服务,服务中要暴露可以调用的远程方法,以接口的形式表现。这样在客户端可以通过服务接口来调用远程方法,实现复杂的业务逻辑。在服务器端,首先要对接口中提供的方法实现,以便客户端调用能够完成一定的业务逻辑;
2、接着需要生成Skeleton,在Skeleton中真正地实现了对商业方法的调用,完成了客户请求的调用的过程,将获取到的调用方法的结果通过序列化机制返回给客户端,进行应答。
3、在客户端,通过Stub来接收服务器返回的数据(对象),即在这里进行了反序列化,也就是读取网络传输的字节流,进而进行重构。在Skeleton和Stub中,都对网络通信进行了处理,例如建立套接字,建立网络连接,为实际的业务需要做好准备。
(1)方法调用方式不同:
RMI中是通过在客户端的Stub对象作为远程接口进行远程方法的调用。每个远程方法都具有方法签名。如果一个方法在服务器上执行,但是没有相匹配的签名被添加到这个远程接口(stub)上,那么这个新方法就不能被RMI客户方所调用。
RPC中是通过网络服务协议向远程主机发送请求,请求包含了一个参数集和一个文本值,通常形成“classname.methodname(参数集)”的形式。RPC远程主机就去搜索与之相匹配的类和方法,找到后就执行方法并把结果编码,通过网络协议发回。
(2)适用语言范围不同:
RMI只用于Java
RPC是网络服务协议,与操作系统和语言无关
(3)调用结果的返回形式不同:
Java是面向对象的,所以RMI的调用结果可以是对象类型或者基本数据类型
RMI的结果统一由外部数据表示 (External Data Representation, XDR) 语言表示,这种语言抽象了字节序类和数据类型结构之间的差异
2.2.2.4 JMS
JMS是Java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。JMS支持两种消息模型:Point-to-Point(P2P)和Publish/Subscribe(Pub/Sub),即点对点和发布订阅模型。在JMS API出现之前,大部分产品使用“点对点”和“发布/订阅”中的任一方式来进行消息通讯。JMS定义了这两种消息发送模型的规范,它们相互独立。任何JMS的提供者可以实现其中的一种或两种模型,这是它们自己的选择。JMS规范提供了通用接口保证我们基于JMS API编写的程序适用于任何一种模型。
(1)Point-to-Point Messaging Domain(点对点通信模型)
a、模式图: b、涉及到的概念: 在点对点通信模式中,应用程序由消息队列,发送方,接收方组成。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。
c、特点:
- 每个消息只要一个消费者
- 发送者和接收者在时间上是没有时间的约束,也就是说发送者在发送完消息之后,不管接收者有没有接受消息,都不会影响发送方发送消息到消息队列中。
- 发送方不管是否在发送消息,接收方都可以从消息队列中取到消息(The receiver can fetch message whether it is running or not when the sender sends the message)
- 接收方在接收完消息之后,需要向消息队列应答成功
(2)Publish/Subscribe Messaging Domain(发布/订阅通信模型)
a、模式图: b、涉及到的概念:
在发布/订阅消息模型中,发布者发布一个消息,该消息通过topic传递给所有的客户端。该模式下,发布者与订阅者都是匿名的,即发布者与订阅者都不知道对方是谁。并且可以动态的发布与订阅Topic。Topic主要用于保存和传递消息,且会一直保存消息直到消息被传递给客户端。
c、特点:
- 一个消息可以传递个多个订阅者(即:一个消息可以有多个接受方)
- 发布者与订阅者具有时间约束,针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息,而且为了消费消息,订阅者必须保持运行的状态。
- 为了缓和这样严格的时间相关性,JMS允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。
JMS、AMQP、MQTT三者区别:
JMS:异步的消息机制,面向企业级应用,只应用在基于JVM的语言,没有标准的底层协议
AMQP:需要跨平台或语言或需要分布式事务的场景下可以使用
MQTT:专为小型无声设备之间通过低带宽发送短消息而设计,多应用于物联网设备,协议简单、流量小、计算量小、能耗低
3 Java线程间通信方式
3.1 同步
这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。
参考示例:
public class MyObject {
synchronized public void methodA() {
//do something....
}
synchronized public void methodB() {
//do some other thing
}
}
public class ThreadA extends Thread {
private MyObject object;
//省略构造方法
@Override
public void run() {
super.run();
object.methodA();
}
}
public class ThreadB extends Thread {
private MyObject object;
//省略构造方法
@Override
public void run() {
super.run();
object.methodB();
}
}
public class Run {
public static void main(String[] args) {
MyObject object = new MyObject();
//线程A与线程B 持有的是同一个对象:object
ThreadA a = new ThreadA(object);
ThreadB b = new ThreadB(object);
a.start();
b.start();
}
}
由于线程A和线程B持有同一个MyObject类的对象object,尽管这两个线程需要调用不同的方法,但是它们是同步执行的,比如:线程B需要等待线程A执行完了methodA()方法之后,它才能执行methodB()方法。这样,线程A和线程B就实现了通信。
这种方式,本质上就是“共享内存”式的通信。多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。
3.2 while轮询的方式
代码如下:
import java.util.ArrayList;
import java.util.List;
public class MyList {
private List<String> list = new ArrayList<String>();
public void add() {
list.add("elements");
}
public int size() {
return list.size();
}
}
import mylist.MyList;
public class ThreadA extends Thread {
private MyList list;
public ThreadA(MyList list) {
super();
this.list = list;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
list.add();
System.out.println("添加了" + (i + 1) + "个元素");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import mylist.MyList;
public class ThreadB extends Thread {
private MyList list;
public ThreadB(MyList list) {
super();
this.list = list;
}
@Override
public void run() {
try {
while (true) {
if (list.size() == 5) {
System.out.println("==5, 线程b准备退出了");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import mylist.MyList;
import extthread.ThreadA;
import extthread.ThreadB;
public class Test {
public static void main(String[] args) {
MyList service = new MyList();
ThreadA a = new ThreadA(service);
a.setName("A");
a.start();
ThreadB b = new ThreadB(service);
b.setName("B");
b.start();
}
}
在这种方式下,线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源。之所以说它浪费资源,是因为JVM调度器将CPU交给线程B执行时,它没做什么“有用”的工作,只是在不断地测试某个条件是否成立。就类似于现实生活中,某个人一直看着手机屏幕是否有电话来了,而不是: 在干别的事情,当有电话来时,响铃通知TA电话来了。关于线程的轮询的影响。
这种方式还存在另外一个问题:
线程都是先把变量读取到本地线程栈空间,然后再去再去修改的本地变量。因此,如果线程B每次都在取本地的条件变量,那么尽管另外一个线程已经改变了轮询的条件,它也察觉不到,这样也会造成死循环。
3.3 wait/notify机制
代码如下:
import java.util.ArrayList;
import java.util.List;
public class MyList {
private static List<String> list = new ArrayList<String>();
public static void add() {
list.add("anyString");
}
public static int size() {
return list.size();
}
}
public class ThreadA extends Thread {
private Object lock;
public ThreadA(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
if (MyList.size() != 5) {
System.out.println("wait begin "
+ System.currentTimeMillis());
lock.wait();
System.out.println("wait end "
+ System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadB extends Thread {
private Object lock;
public ThreadB(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
MyList.add();
if (MyList.size() == 5) {
lock.notify();
System.out.println("已经发出了通知");
}
System.out.println("添加了" + (i + 1) + "个元素!");
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Run {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA a = new ThreadA(lock);
a.start();
Thread.sleep(50);
ThreadB b = new ThreadB(lock);
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程A要等待某个条件满足时(list.size()==5),才执行操作。线程B则向list中添加元素,改变list 的size。
A,B之间如何通信的呢?也就是说,线程A如何知道 list.size() 已经为5了呢?
这里用到了Object类的 wait() 和 notify() 方法。
当条件未满足时(list.size() !=5),线程A调用wait() 放弃CPU,并进入阻塞状态。——不像while轮询那样占用CPU
当条件满足时,线程B调用 notify()通知线程A,所谓通知线程A,就是唤醒线程A,并让它进入可运行状态。
这种方式的一个好处就是CPU的利用率提高了。
但是也有一些缺点:比如,线程B先执行,一下子添加了5个元素并调用了notify()发送了通知,而此时线程A还执行;当线程A执行并调用wait()时,那它永远就不可能被唤醒了。因为,线程B已经发了通知了,以后不再发通知了。这说明:通知过早,会打乱程序的执行逻辑。
3.4 管道通信
使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信,分布式系统中说的两种通信机制:共享内存机制和消息通信机制。synchronized关键字和while轮询 “属于” 共享内存机制,由于是轮询的条件使用了volatile关键字修饰时,这就表示它们通过判断这个“共享的条件变量“是否改变了,来实现线程间的交流。而管道通信,更像消息传递机制,也就是说:通过管道,将一个线程中的消息发送给另一个。
4 总结
4.1 广播机制
4.1.1 观察者模式
观察者模式:在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。
观察者模式是广播机制,消息订阅和推送的核心设计。
开发中常见的场景:
1.手机中消息推送
2.后台群发信息
…
观察者模式主要有两个角色
- Subject 观察主题对象,也可以叫被观察或者被订阅对象
- Observer 观察者或者订阅者对象,当
Subject 有变动,就会通知到每一个Observer
上述2.2.2.4中的JMS就是采用观察者模式实现的。
4.2.2 安卓应用(补充)
4.2.2.1 标准广播
完全异步执行的广播,当发出广播后,广播接收器几乎会在同一时刻接收到广播消息,所以没有先后顺序可言,效率比较高,无法被截断。
4.2.2.2 有序广播
同步执行的广播,广播发出后,会有一个广播接收器接收广播消息,当这个广播接收器中的逻辑执行完毕后广播才会继续传递。有先后顺序,优先级较高的接收器先收到广播消息并且可以截断正在传递的广播,使得后面的接收器无法收到广播消息。
4.2.2.3 系统广播
Android内置很多系统级别广播,如手机开机后发一条广播,电池电量发生变化发一条广播等等
4.2.2.4 自定义广播
4.2.2.5 本地广播
(出于安全性考虑,只能在程序内部传递)
优势:正在发送的广播不会离开我们的程序,不必担心数据泄露。其他程序无法将广播发送到我们程序内部,不必担心安全漏洞。发送本地广播比系统全局广播更高效。
4.2 共享存储、长轮询
如3.1 线程同步和3.2 while轮询中的内容
4.3 管道模式
如3.4 管道通信
4.4 等待、通知机制
如3.3 wait/notify机制
4.5 远程通信
如2.2.2所示RPC、WebService、RMI以及JMS
1.文章中所有例程均已上传,资源链接:https://download.csdn.net/download/qq_38233258/85008235 2.文中部分图片、代码引用于其他博主博客,仅用作学习用途,在此表示感谢,如有侵权,可联系我删除。
|