Java语言内置了多线程支持。当我们启动一个Java程序时,实际上是启动了一个JVM进程,然后JVM启动主线程来执行
本质上来讲,在Java中创建新线程十分简单,只需要实例化一个
1 | Thread t = new Thread(); |
但是当我们调用该实例的
1 2 3 4 5 6 7 8 9 10 | @Override public void run() { if (target != null) { target.run(); } } |
这里的
在Java中,要创建能执行指定代码的新线程,有以下几种方法:
- 从
Thread 类派生一个自定义类,然后覆写run() 方法。 - 创建
Thread 实例时,传入一个Runnadble 接口的实例,同样要实现接口定义的run() 抽象方法。 - 实现Callable接口,重写
call() 方法,并包装成FutureTask 类对象,再作为参数传入Thread 的构造器。 - 使用线程池创建新线程。
注意:
- 方法1和方法2因为都是通过覆写
run() 方法来创建新线程,而run() 方法并无返回值,所以这两种方法也无法获取返回值。 - 在实际项目工程中,最好不要在应用中自行显式创建线程(方法1、2、3),线程资源要通过线程池提供。使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
1. 继承Thread 类,覆写run() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public class ExecuteThread { public static void main(String[] args) { Thread t = new MyThread(); t.start(); // 启动新线程 } } // 继承自Thread的自定义类 class MyThread extends Thread { @Override public void run() { System.out.println("start new thread!"); } } |
如果该类型的线程只需要使用一次,也可以用匿名内部类的方式让代码更加简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class ExecuteThread { public static void main(String[] args) { Thread t = new Thread() { // 内部匿名类对run()方法的覆写 @Override public void run() { System.out.println("start new thread!"); } }; t.start(); // 启动新线程 } } |
- 优点:编码简单,容易理解。
- 缺点:不能继承其他类,功能单一。
2. 实现Runnadble 接口,实现run() 抽象方法
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class ExecuteThread { public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); // 启动新线程 } } class MyRunnable implements Runnable { @Override public void run() { System.out.println("start new thread!"); } } |
或者用Java8引入的Lambda语法进一步简写为:
1 2 3 4 5 6 7 8 | public class ExecuteThread { public static void main(String[] args) { Thread t = new Thread(() -> { System.out.println("start new thread!"); }); t.start(); // 启动新线程 } } |
- 优点:
- 传入的实例实现的是
Runnadble 接口,所以可以继承其他的类,避免了单继承的局限性。 - 适合多个相同程序代码的线程共享一个资源(同一个线程任务对象根据传入构造器的不同
Runnadble 接口实例,可以被包装成多个线程对象),实现解耦操作,代码和线程独立。
- 传入的实例实现的是
- 缺点:实现相对复杂一些。
3. 实现Callable接口,重写call() 方法,并包装成FutureTask 对象传入Thread 构造器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public class ExecuteThread { public static void main(String[] args) throws ExecutionException, InterruptedException { Callable<String> callableInstance = new MyCallable<>("return value"); // 使用FutureTask类包装Callable接口的实例,该对象封装了Callable接口实现实例的call()方法的返回值。 FutureTask<String> task = new FutureTask<>(callableInstance); Thread t = new Thread(task); t.start(); // 启动新线程 // 调用FutureTask实例的get()方法获取新线程执行结束返回值。 System.out.println(task.get()); } } class MyCallable<V> implements Callable<V> { private V toReturn; public MyCallable (V val) { toReturn = val; } @Override public V call() throws Exception{ System.out.println("start new thread!"); return toReturn; } } |
- 优点:相对于方法2(传入
Runnadble 实例),可以获取返回值。 - 缺点:实现比较复杂。
4. 使用线程池创建新线程
线程池的使用避免了因为频繁创建、销毁线程带来的大量系统开销,实现了资源的复用。具体的使用方法看这篇笔记:Java多线程学习笔记——如何使用线程池。
参考链接
- Java多线程实现的四种方式
- 创建新线程-廖雪峰的官方网站
{"s":1,"m":"ok","d":{"entryViewId":"5f154d6ae51d45348a2b7d1c","entryId":"5f154d6a6fb9a07ea3636804","content":"
前言
什么是RPC服务
RPC,是Remote Procedure Call的简称,翻译成中文就是远程过程调用。RPC就是允许程序调用另一个地址空间(通常是另一台机器上)的类方法或函数的一种服务。
它是一种架设在计算机网络之上并隐藏底层网络技术,可以像调用本地服务一样调用远端程序,在编码代价不高的情况下提升吞吐的能力。
为什么要使用RPC服务
随着计算机技术的快速发展,单台机器运行服务的方案已经不足以支撑越来越多的网络请求负载,分布式方案开始兴起,一个业务场景可以被拆分在多个机器上运行,每个机器分别只完成一个或几个的业务模块。为了能让其他机器使用某台机器中的业务模块方法,就有了RPC服务,它是基于一种专门实现远程方法调用的协议上完成的服务。现如今很多主流语言都支持RPC服务,常用的有Java的Dubbo、Go的net/rpc & RPCX、谷歌的gRPC等。
关于gRPC
大部分RPC都是基于socket实现的,可以比http请求来的高效。gRPC是谷歌开发并开源的一款实现RPC服务的高性能框架,它是基于http2.0协议的,目前已经支持C、C++、Java、Node.js、Python、Ruby、Objective-C、PHP和C#等等语言。要将方法调用以及调用参数,响应参数等在两个服务器之间进行传输,就需要将这些参数序列化,gRPC采用的是protocol buffer的语法(检查proto),通过proto语法可以定义好要调用的方法、和参数以及响应格式,可以很方便地完成远程方法调用,而且非常利于扩展和更新参数。
快速上手gRPC
使用gRPC实现远程方法调用之前,我们需要了解protocol buffer语法,安装支持protocol buffer语法编译成.proto文件的工具,然后再完成gRPC的服务端(远程方法提供者)和客户端(调用者)的搭建和封装。
了解protocol buffer
Protocol Buffer是Google的跨语言,跨平台,可扩展机制的,用于序列化结构化数据 - 对比XML,但更小,更快,更简单的一种数据格式。您可以定义数据的结构化,例如方法的名字、参数和响应格式等,然后可以使用对应的语言工具生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。
语法使用
- 定义消息类型
1 2 3 4 5 6 7 8 | package test; syntax = "proto3"; message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; } |
上面的例子就是一个.proto文件,该文件的第一行指定包名,方便您在别的proto文件中import这个文件的定义,第二行是您正在使用proto3语法:如果您不这样做,protobuf 编译器将假定您正在使用proto2。这必须是文件的第一个非空的非注释行,目前建议使用proto3语法。
SearchRequest是消息体的名字,指定了三个字段,分别指定了字段的类型和顺序,顺序必须从1开始,并且不可重复;
- 指定字段规则
消息字段可以是以下之一:
单数(默认):格式良好的消息可以包含该字段中的零个或一个(但不超过一个)。
repeated:此字段可以在格式良好的消息中重复任意次数(包括零)。将保留重复值的顺序。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 | syntax = "proto3"; message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; repeated Body body = 4; } message Body { int32 id = 1; string number = 2; } |
上述例子其实就是定义了一个格式,用我们通常的json格式表示就是:
1 2 3 4 5 6 7 8 9 10 11 | { "query": str, "page_number":int, "result_per_page":int, "body":[ { "id":int, "number":str } ], } |
- 标量值类型
标量消息字段可以具有以下类型之一 - 该表显示.proto文件中指定的类型,以及自动生成的类中的相应类型:
| .proto Type | 备注 | Python Typ |
|---|---|---|
| double | float | |
| float | float | |
| int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 | int |
| uint32 | 使用变长编码 | int/long |
| uint64 | 使用变长编码 | int/long |
| sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int |
| sint64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 | int/long |
| fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 | int |
| fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 | int/long |
| sfixed32 | 总是4个字节 | int |
| sfixed64 | 总是8个字节 | int/long |
| bool | 布尔值 | bool |
| string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | str/unicode |
| bytes | 可能包含任意顺序的字节数据。 | str |
- 默认值
解析消息时,如果编码消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于bools,默认值为false。
- 对于数字类型,默认值为零。
- 对于枚举,默认值是第一个定义的枚举值,该值必须为0。
- 重复字段的默认值为空(通常是相应语言的空列表)
- 枚举类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus corpus = 4; } |
Corpus枚举的第一个常量映射为零:每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为:
- 必须有一个零值,以便我们可以使用0作为数字默认值。
- 零值必须是第一个元素,以便与proto2语义兼容,其中第一个枚举值始终是默认值。
- 定义方法
1 2 3 | service SearchService { rpc Search(SearchRequest)returns(SearchResponse); } |
上面的语句就定义好了远程调用的方法名Search,待编译好对应语言的源代码之后就可以使用远程调用,例如在Python中初始化SearchService方法,则执行Search方法,就是采用SearchRequest的格式去调用远程机器的方法,然后按定义好的SearchResponse格式返回调用结果。根据proto的语法定义,甚至可以实现跨平台,跨语言使用这种远程调用。
使用工具生成对应语言的源代码
根据实际工作需要,生成以下对应语言的自定义消息类型Java,Python,C ++,Go, Ruby, Objective-C,或C#的.proto文件,你需要运行protobuf 编译器protoc上.proto。如果尚未安装编译器,请下载该软件包并按照自述文件中的说明进行操作。
Protobuf 编译器的调用如下:
1 | protoc --proto_path = IMPORT_PATH --cpp_out = DST_DIR --java_out = DST_DIR --python_out = DST_DIR --go_out = DST_DIR --ruby_out = DST_DIR --objc_out = DST_DIR --csharp_out = DST_DIR path / to / file .proto |
Python生成对应的源代码
- 安装Python的gRPC源码包grpcio,用于执行gRPC的各种底层协议和请求响应方法
- 安装Python基于gRPC的proto生成python源代码的工具grpcio-tools
1 2 3 | sudo python -m pip install grpcio python -m pip install grpcio-tools |
- 执行编译生成python的proto序列化协议源代码:
1 2 3 4 5 6 7 | # 编译 proto 文件 python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. test.proto python -m grpc_tools.protoc: python 下的 protoc 编译器通过 python 模块(module) 实现, 所以说这一步非常省心 --python_out=. : 编译生成处理 protobuf 相关的代码的路径, 这里生成到当前目录 --grpc_python_out=. : 编译生成处理 grpc 相关的代码的路径, 这里生成到当前目录 -I. test.proto : proto 文件的路径, 这里的 proto 文件在当前目录 |
编译后生成的源代码:
- test_pb2.py: 用来和 protobuf 数据进行交互,这个就是根据proto文件定义好的数据结构类型生成的python化的数据结构文件
- test_pb2_grpc.py: 用来和 grpc 进行交互,这个就是定义了rpc方法的类,包含了类的请求参数和响应等等,可用python直接实例化调用
搭建Python gRPC服务
生成好了python可以直接实例化和调用的gRPC类,我们就可以开始搭建RPC的服务端(远程调用提供者)和客户端(调用者)了。
- 搭建服务端server.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | from concurrent import futures import time import grpc import test_pb2 import test_pb2_grpc # 实现 proto 文件中定义的 SearchService class RequestRpc(test_pb2_grpc.SearchService): # 实现 proto 文件中定义的 rpc 调用 def doRequest(self, request, context): return test_pb2.Search(query = 'hello {msg}'.format(msg = request.name)) # return的数据是符合定义的SearchResponse格式 def serve(): # 启动 rpc 服务,这里可定义最大接收和发送大小(单位M),默认只有4M server = grpc.server(futures.ThreadPoolExecutor(max_workers=10), options=[ ('grpc.max_send_message_length', 100 * 1024 * 1024), ('grpc.max_receive_message_length', 100 * 1024 * 1024)]) test_pb2_grpc.add_SearchServiceServicer_to_server(RequestRpc(), server) server.add_insecure_port('[::]:50051') server.start() try: while True: time.sleep(60*60*24) # one day in seconds except KeyboardInterrupt: server.stop(0) if __name__ == '__main__': serve() |
- 搭建客户端client.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import grpc import helloworld_pb2 import helloworld_pb2_grpc def run(): # 连接 rpc 服务器 channel = grpc.insecure_channel('localhost:50051') # 调用 rpc 服务 stub = test_pb2_grpc.SearchServiceStub(channel) response = stub.doRequest(test_pb2.SearchRequest(query='henry')) print("client received: ", response) if __name__ == '__main__': run() |
最佳实践
- 编写proto文件的时候,注意定义好数据的格式,要多考虑可扩张性,例如可以定义api_version等用于区分版本,防止未来的版本有大的数据格式更新的时候可以兼容;
- 对于不可变类型,建议使用枚举,例如请求一个字段type,取值是固定的时候,可以用枚举类型;
- 对于服务端和客户端的编写,建议指定好最大接收和发送大小,避免出现数据溢出的异常;
- gRPC偶尔会出现断线重连的情况,所以要增加异常处理机制,捕获到由于重连时引发远程调用失败的问题,则可以执行重试(会在接下来的文章中详细说明);
- gRPC可以采用SSL或TLS的协议,实现http2.0加密传输,提高系统的安全性(会在接下来的文章中详细说明);
- 对于流量、并发较大的服务,可以通过微服务的一些应用或组件(如istio)等实现流量的熔断、限流等等,提高稳定性。
gRPC的优势
性能
gRPC消息使用一种有效的二进制消息格式protobuf进行序列化。Protobuf在服务器和客户机上的序列化非常快。Protobuf序列化后的消息体积很小,能够有效负载,在移动应用程序等有限带宽场景中显得很重要。
gRPC是为HTTP/2而设计的,它是HTTP的一个主要版本,与HTTP 1.x相比具有显著的性能优势:
- 二进制框架和压缩。HTTP/2协议在发送和接收方面都很紧凑和高效。
- 通过单个TCP连接复用多个HTTP/2调用。多路复用消除了线头阻塞。
代码生成
所有gRPC框架都为代码生成提供了一流的支持。gRPC开发的核心文件是*.proto文件 ,它定义了gRPC服务和消息的约定。根据这个文件,gRPC框架将生成服务基类,消息和完整的客户端代码。
通过在服务器和客户端之间共享*.proto文件,可以从端到端生成消息和客户端代码。客户端的代码生成消除了客户端和服务器上的重复消息,并为您创建了一个强类型的客户端。无需编写客户端代码,可在具有许多服务的应用程序中节省大量开发时间。
严格的规范
不存在具有JSON的HTTP API的正式规范。开发人员不需要讨论URL,HTTP动词和响应代码的最佳格式。(想想,是用Post还是Get好?使用Get还是用Put好?一想到有选择恐惧症的你是不是又开了纠结,然后浪费了大量的时间)
该gRPC规范是规定有关gRPC服务必须遵循的格式。gRPC消除了争论并节省了开发人员的时间,因为gPRC在各个平台和实现之间是一致的。
流
HTTP/2为长期的实时通信流提供了基础。gRPC通过HTTP/2为流媒体提供一流的支持。
gRPC服务支持所有流组合:
- 一元(没有流媒体)
- 服务器到客户端流
- 客户端到服务器流
- 双向流媒体
截至时间/超时和取消
gRPC允许客户端指定他们愿意等待RPC完成的时间。该期限被发送到服务端,服务端可以决定在超出了限期时采取什么行动。例如,服务器可能会在超时时取消正在进行的gRPC / HTTP /数据库请求。
通过子gRPC调用截至时间和取消操作有助于实施资源使用限制。
推荐使用gRPC的场景
- 微服务 - gRPC设计为低延迟和高吞吐量通信。gRPC非常适用于效率至关重要的轻型微服务。
点对点实时通信 - gRPC对双向流媒体提供出色的支持。gRPC服务可以实时推送消息而无需轮询。
多语言混合开发环境 - gRPC工具支持所有流行的开发语言,使gRPC成为多语言开发环境的理想选择。 - 网络受限环境 - 使用Protobuf(一种轻量级消息格式)序列化gRPC消息。gRPC消息始终小于等效的JSON消息。
参考文献
- https://juejin.im/post/5bb597c2e51d450e6e03e42d
- https://doc.oschina.net/grpc?t=58008
- https://juejin.im/post/5c86148ce51d45206776864e
- https://www.jianshu.com/p/43fdfeb105ff