广告
返回顶部
首页 > 资讯 > 精选 >Java 并发框架的介绍和使用方法
  • 705
分享到

Java 并发框架的介绍和使用方法

2023-06-03 08:06:16 705人浏览 八月长安
摘要

这篇文章主要讲解了“ Java 并发框架的介绍和使用方法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“ Java 并发框架的介绍和使用方法”吧! 为什么要写这篇文章几年前 NoSQL 开始流

这篇文章主要讲解了“ Java 并发框架的介绍和使用方法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“ Java 并发框架的介绍和使用方法”吧!

为什么要写这篇文章

几年前 NoSQL 开始流行的时候,像其他团队一样,我们的团队也热衷于令人兴奋的新东西,并且计划替换一个应用程序的数据库。但是,当深入实现细节时,我们想起了一位智者曾经说过的话:“细节决定成败”。最终我们意识到 NoSQL 不是解决所有问题的银弹,而 Nosql vs RDMS 的答案是:“视情况而定”。类似地,去年RxJava 和 spring Reactor 这样的并发库加入了让人充满激情的语句,如异步非阻塞方法等。为了避免再犯同样的错误,我们尝试评估诸如 ExecutorService、 RxJava、Disruptor 和 Akka 这些并发框架彼此之间的差异,以及如何确定各自框架的正确用法。

本文中用到的术语在这里有更详细的描述。

2. 分析并发框架的示例用例

Java 并发框架的介绍和使用方法

3. 快速更新线程配置

在开始比较并发框架的之前,让我们快速复习一下如何配置最佳线程数以提高并行任务的性能。这个理论适用于所有框架,并且在所有框架中使用相同的线程配置来度量性能。

  • 对于内存任务,线程的数量大约等于具有最佳性能的内核的数量,尽管它可以根据各自处理器中的超线程特性进行一些更改。

    • 例如,在8核机器中,如果对应用程序的每个请求都必须在内存中并行执行4个任务,那么这台机器上的负载应该保持为 @2 req/sec,在 ThreadPool 中保持8个线程。

  • 对于 I/O 任务,ExecutorService 中配置的线程数应该取决于外部服务的延迟。

    • 与内存中的任务不同,I/O 任务中涉及的线程将被阻塞,并处于等待状态,直到外部服务响应或超时。因此,当涉及 I/O 任务线程被阻塞时,应该增加线程的数量,以处理来自并发请求的额外负载。

    • I/O 任务的线程数应该以保守的方式增加,因为处于活动状态的许多线程带来了上下文切换的成本,这将影响应用程序的性能。为了避免这种情况,应该根据 I/O 任务中涉及的线程的等待时间按比例增加此机器的线程的确切数量以及负载。

参考: Http://baddotrobot.com/blog/2013/06/01/optimum-number-of-threads/

4. 性能测试结果

性能测试配置 GCP -> 处理器:Intel(R) Xeon(R) CPU @ 2.30GHz;架构:x86_64;CPU 内核:8个(注意:这些结果仅对该配置有意义,并不表示一个框架比另一个框架更好)。

Java 并发框架的介绍和使用方法

5. 使用执行器服务并行化 io 任务

5.1 何时使用?

如果一个应用程序部署在多个节点上,并且每个节点的 req/sec 小于可用的核心数量,那么 ExecutorService 可用于并行化任务,更快地执行代码。

5.2 什么时候适用?

如果一个应用程序部署在多个节点上,并且每个节点的 req/sec 远远高于可用的核心数量,那么使用 ExecutorService 进一步并行化只会使情况变得更糟。

当外部服务延迟增加到 400ms 时,性能测试结果如下(请求速率 @50 req/sec,8核)。

Java 并发框架的介绍和使用方法

3 所有任务按顺序执行示例

// I/O 任务:调用外部服务
String posts = JSONService.getPosts();
String comments = jsonService.getComments();
String albums = JsonService.getAlbums();
String photos = JsonService.getPhotos();

// 合并来自外部服务的响应
// (内存中的任务将作为此操作的一部分执行)
int userId = new Random().nextInt(10) + 1;
String postsAndCommentsOfRandomUser = ResponseUtil.getPostsAndCommentsOfRandomUser(userId, posts, comments);
String albumsAndPhotosOfRandomUser = ResponseUtil.getAlbumsAndPhotosOfRandomUser(userId, albums, photos);

// 构建最终响应并将其发送回客户端
String response = postsAndCommentsOfRandomUser + albumsAndPhotosOfRandomUser;
return response;

5.4 I/O 任务与 ExecutorService 并行执行代码示例

// 添加 I/O 任务
List<Callable<String>> iocallableTasks = new ArrayList<>();
ioCallableTasks.add(JsonService::getPosts);
ioCallableTasks.add(JsonService::getComments);
ioCallableTasks.add(JsonService::getAlbums);
ioCallableTasks.add(JsonService::getPhotos);

// 调用所有并行任务
ExecutorService ioExecutorService = CustomThreads.getExecutorService(ioPoolSize);
List<Future<String>> futuresOfIOTasks = ioExecutorService.invokeAll(ioCallableTasks);

// 获取 I/O  操作(阻塞调用)结果
String posts = futuresOfIOTasks.get(0).get();
String comments = futuresOfIOTasks.get(1).get();
String albums = futuresOfIOTasks.get(2).get();
String photos = futuresOfIOTasks.get(3).get();

// 合并响应(内存中的任务是此操作的一部分)
String postsAndCommentsOfRandomUser = ResponseUtil.getPostsAndCommentsOfRandomUser(userId, posts, comments);
String albumsAndPhotosOfRandomUser = ResponseUtil.getAlbumsAndPhotosOfRandomUser(userId, albums, photos);

// 构建最终响应并将其发送回客户端
return postsAndCommentsOfRandomUser + albumsAndPhotosOfRandomUser;

6. 使用执行器服务并行化 IO 任务(CompletableFuture)

与上述情况类似:处理传入请求的 HTTP 线程被阻塞,而 CompletableFuture 用于处理并行任务

6.1 何时使用?

如果没有 AsyncResponse,性能与 ExecutorService 相同。如果多个 api 调用必须异步并且链接起来,那么这种方法更好(类似 node 中的 Promises)。

ExecutorService ioExecutorService = CustomThreads.getExecutorService(ioPoolSize);

// I/O 任务
CompletableFuture<String> postsFuture = CompletableFuture.supplyAsync(JsonService::getPosts, ioExecutorService);
CompletableFuture<String> commentsFuture = CompletableFuture.supplyAsync(JsonService::getComments,
   ioExecutorService);
CompletableFuture<String> albumsFuture = CompletableFuture.supplyAsync(JsonService::getAlbums,
   ioExecutorService);
CompletableFuture<String> photosFuture = CompletableFuture.supplyAsync(JsonService::getPhotos,
   ioExecutorService);
CompletableFuture.allOf(postsFuture, commentsFuture, albumsFuture, photosFuture).get();

// 从 I/O 任务(阻塞调用)获得响应
String posts = postsFuture.get();
String comments = commentsFuture.get();
String albums = albumsFuture.get();
String photos = photosFuture.get();

// 合并响应(内存中的任务将是此操作的一部分)
String postsAndCommentsOfRandomUser = ResponseUtil.getPostsAndCommentsOfRandomUser(userId, posts, comments);
String albumsAndPhotosOfRandomUser = ResponseUtil.getAlbumsAndPhotosOfRandomUser(userId, albums, photos);

// 构建最终响应并将其发送回客户端
return postsAndCommentsOfRandomUser + albumsAndPhotosOfRandomUser;

7. 使用 ExecutorService 并行处理所有任务

使用 ExecutorService 并行处理所有任务,并使用 @suspended AsyncResponse response 以非阻塞方式发送响应。

Java 并发框架的介绍和使用方法

图片来自 http://tutorials.jenkov.com/java-NIO/nio-vs-io.html

  • HTTP 线程处理传入请求的连接,并将处理传递给 Executor Pool,当所有任务完成后,另一个 HTTP 线程将把响应发送回客户端(异步非阻塞)。

  • 性能下降原因:

    • 在同步通信中,尽管 I/O 任务中涉及的线程被阻塞,但是只要进程有额外的线程来承担并发请求负载,它仍然处于运行状态。

    • 因此,以非阻塞方式保持线程所带来的好处非常少,而且在此模式中处理请求所涉及的成本似乎很高。

    • 通常,对这里讨论采用的例子使用异步非阻塞方法会降低应用程序的性能。

7.1 何时使用?

如果用例类似于服务器端聊天应用程序,在客户端响应之前,线程不需要保持连接,那么异步、非阻塞方法比同步通信更受欢迎。在这些用例中,系统资源可以通过异步、非阻塞方法得到更好的利用,而不仅仅是等待。

// 为异步执行提交并行任务
ExecutorService ioExecutorService = CustomThreads.getExecutorService(ioPoolSize);
CompletableFuture<String> postsFuture = CompletableFuture.supplyAsync(JsonService::getPosts, ioExecutorService);
CompletableFuture<String> commentsFuture = CompletableFuture.supplyAsync(JsonService::getComments,
ioExecutorService);
CompletableFuture<String> albumsFuture = CompletableFuture.supplyAsync(JsonService::getAlbums,
ioExecutorService);
CompletableFuture<String> photosFuture = CompletableFuture.supplyAsync(JsonService::getPhotos,
ioExecutorService);

// 当 /posts API 返回响应时,它将与来自 /comments API 的响应结合在一起
// 作为这个操作的一部分,将执行内存中的一些任务
CompletableFuture<String> postsAndCommentsFuture = postsFuture.thenCombineAsync(commentsFuture,
(posts, comments) -> ResponseUtil.getPostsAndCommentsOfRandomUser(userId, posts, comments),
ioExecutorService);

// 当 /albums API 返回响应时,它将与来自 /photos API 的响应结合在一起
// 作为这个操作的一部分,将执行内存中的一些任务
CompletableFuture<String> albumsAndPhotosFuture = albumsFuture.thenCombineAsync(photosFuture,
(albums, photos) -> ResponseUtil.getAlbumsAndPhotosOfRandomUser(userId, albums, photos),
ioExecutorService);

// 构建最终响应并恢复 http 连接,把响应发送回客户端
postsAndCommentsFuture.thenAcceptBothAsync(albumsAndPhotosFuture, (s1, s2) -> {
LOG.info("Building Async Response in Thread " + Thread.currentThread().getName());
String response = s1 + s2;
asyncHttpResponse.resume(response);
}, ioExecutorService);

8. RxJava

  • 这与上面的情况类似,唯一的区别是 RxJava 提供了更好的 DSL 可以进行流式编程,下面的例子中没有体现这一点。

  • 性能优于 CompletableFuture 处理并行任务。

8.1 何时使用?

如果编码的场景适合异步非阻塞方式,那么可以首选 RxJava 或任何响应式开发库。还具有诸如 back-pressure 之类的附加功能,可以在生产者和消费者之间平衡负载。

int userId = new Random().nextInt(10) + 1;
ExecutorService executor = CustomThreads.getExecutorService(8);

// I/O 任务
Observable<String> postsObservable = Observable.just(userId).map(o -> JsonService.getPosts())
.subscribeOn(Schedulers.from(executor));
Observable<String> commentsObservable = Observable.just(userId).map(o -> JsonService.getComments())
.subscribeOn(Schedulers.from(executor));
Observable<String> albumsObservable = Observable.just(userId).map(o -> JsonService.getAlbums())
.subscribeOn(Schedulers.from(executor));
Observable<String> photosObservable = Observable.just(userId).map(o -> JsonService.getPhotos())
.subscribeOn(Schedulers.from(executor));

// 合并来自 /posts 和 /comments API 的响应
// 作为这个操作的一部分,将执行内存中的一些任务
Observable<String> postsAndCommentsObservable = Observable
.zip(postsObservable, commentsObservable,
(posts, comments) -> ResponseUtil.getPostsAndCommentsOfRandomUser(userId, posts, comments))
.subscribeOn(Schedulers.from(executor));

// 合并来自 /albums 和 /photos API 的响应
// 作为这个操作的一部分,将执行内存中的一些任务
Observable<String> albumsAndPhotosObservable = Observable
.zip(albumsObservable, photosObservable,
(albums, photos) -> ResponseUtil.getAlbumsAndPhotosOfRandomUser(userId, albums, photos))
.subscribeOn(Schedulers.from(executor));

// 构建最终响应
Observable.zip(postsAndCommentsObservable, albumsAndPhotosObservable, (r1, r2) -> r1 + r2)
.subscribeOn(Schedulers.from(executor))
.subscribe((response) -> asyncResponse.resume(response), e -> asyncResponse.resume("error"));

9. Disruptor

Java 并发框架的介绍和使用方法

[Queue vs RingBuffer]

Java 并发框架的介绍和使用方法

图片1:http://tutorials.jenkov.com/java-concurrency/blocking-queues.html

图片2:https://www.baeldung.com/lmax-disruptor-concurrency

  • 在本例中,HTTP 线程将被阻塞,直到 disruptor 完成任务,并且使用 countdowlatch 将 HTTP 线程与 ExecutorService 中的线程同步。

  • 这个框架的主要特点是在没有任何的情况下处理线程间通信。在 ExecutorService 中,生产者和消费者之间的数据将通过 Queue传递,在生产者和消费者之间的数据传输过程中涉及到一个锁。Disruptor 框架通过一个名为 Ring Buffer 的数据结构(它是循环数组队列的扩展版本)来处理这种生产者-消费者通信,并且不需要任何锁。

  • 这个库不适用于我们在这里讨论的这种用例。仅出于好奇而添加。

9.1 何时使用?

Disruptor 框架在下列场合性能更好:与事件驱动的体系结构一起使用,或主要关注内存任务的单个生产者和多个消费者。

static {
   int userId = new Random().nextInt(10) + 1;

   // 示例 Event-Handler; count down latch 用于使线程与 http 线程同步
   EventHandler<Event> postsApiHandler = (event, sequence, endOfBatch) -> {
       event.posts = JsonService.getPosts();
       event.countDownLatch.countDown();
   };

   // 配置 Disputor 用于处理事件
   DISRUPTOR.handleEventsWith(postsApiHandler, commentsApiHandler, albumsApiHandler)
   .handleEventsWithWorkerPool(photosApiHandler1, photosApiHandler2)
   .thenHandleEventsWithWorkerPool(postsAndCommentsResponseHandler1, postsAndCommentsResponseHandler2)
   .handleEventsWithWorkerPool(albumsAndPhotosResponseHandler1, albumsAndPhotosResponseHandler2);
   DISRUPTOR.start();
}

// 对于每个请求,在 RingBuffer 中发布一个事件:
Event event = null;
RingBuffer<Event> ringBuffer = DISRUPTOR.getRingBuffer();
long sequence = ringBuffer.next();
CountDownLatch countDownLatch = new CountDownLatch(6);
try {
   event = ringBuffer.get(sequence);
   event.countDownLatch = countDownLatch;
   event.startTime = System.currentTimeMillis();
} finally {
   ringBuffer.publish(sequence);
}
try {
   event.countDownLatch.await();
} catch (InterruptedException e) {
   e.printStackTrace();
}

10. Akka

Java 并发框架的介绍和使用方法

图片来自:https://blog.codecentric.de/en/2015/08/introduction-to-akka-actors/

  • Akka 库的主要优势在于它拥有构建分布式系统的本地支持。

  • 它运行在一个叫做 Actor System 的系统上。这个系统抽象了线程的概念,Actor System 中的 Actor 通过异步消息进行通信,这类似于生产者和消费者之间的通信。

  • 这种额外的抽象级别有助于 Actor System 提供诸如容错、位置透明等特性。

  • 使用正确的 Actor-to-Thread 策略,可以对该框架进行优化,使其性能优于上表所示的结果。虽然它不能在单个节点上与传统方法的性能匹敌,但是由于其构建分布式和弹性系统的能力,仍然是首选。

10.1 示例代码

// 来自 controller :
Actors.masterActor.tell(new Master.Request("Get Response", event, Actors.workerActor), ActorRef.noSender());

// handler :
public Receive createReceive() {
   return receiveBuilder().match(Request.class, request -> {
   Event event = request.event; // ideally, immutable data structures should be used here.
   request.worker.tell(new JsonServiceWorker.Request("posts", event), getSelf());
   request.worker.tell(new JsonServiceWorker.Request("comments", event), getSelf());
   request.worker.tell(new JsonServiceWorker.Request("albums", event), getSelf());
   request.worker.tell(new JsonServiceWorker.Request("photos", event), getSelf());
   }).match(Event.class, e -> {
   if (e.posts != null && e.comments != null & e.albums != null & e.photos != null) {
   int userId = new Random().nextInt(10) + 1;
   String postsAndCommentsOfRandomUser = ResponseUtil.getPostsAndCommentsOfRandomUser(userId, e.posts,
   e.comments);
   String albumsAndPhotosOfRandomUser = ResponseUtil.getAlbumsAndPhotosOfRandomUser(userId, e.albums,
   e.photos);
   String response = postsAndCommentsOfRandomUser + albumsAndPhotosOfRandomUser;
   e.response = response;
   e.countDownLatch.countDown();
   }
   }).build();
}

11. 总结

  • 根据机器的负载决定 Executor 框架的配置,并检查是否可以根据应用程序中并行任务的数量进行负载平衡。

  • 对于大多数传统应用程序来说,使用响应式开发库或任何异步库都会降低性能。只有当用例类似于服务器端聊天应用程序时,这个模式才有用,其中线程在客户机响应之前不需要保留连接。

  • Disruptor 框架在与事件驱动的架构模式一起使用时性能很好; 但是当 Disruptor 模式与传统架构混合使用时,就我们在这里讨论的用例而言,它并不符合标准。

感谢各位的阅读,以上就是“ Java 并发框架的介绍和使用方法”的内容了,经过本文的学习后,相信大家对 Java 并发框架的介绍和使用方法这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是编程网,小编将为大家推送更多相关知识点的文章,欢迎关注!

--结束END--

本文标题: Java 并发框架的介绍和使用方法

本文链接: https://www.lsjlt.com/news/233778.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Java 并发框架的介绍和使用方法
    这篇文章主要讲解了“ Java 并发框架的介绍和使用方法”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“ Java 并发框架的介绍和使用方法”吧! 为什么要写这篇文章几年前 NoSQL 开始流...
    99+
    2023-06-03
  • Java并发fork/join框架的介绍及使用
    本篇内容主要讲解“Java并发fork/join框架的介绍及使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java并发fork/join框架的介绍及使用”吧!目录一、概述二、说一说 Recu...
    99+
    2023-06-20
  • Java shiro安全框架使用介绍
    目录1.shiro安全框架1.1 什么是权限管理1.2 什么是身份认证1.3 什么是授权1.4 认证授权框架有哪些2.使用shiro完成认证工作2.1 shiro中认证的关键对象2....
    99+
    2022-11-13
  • 使用Java中的并发库和框架实现高并发
    文章目录 使用Java中的并发库和框架实现高并发背景介绍技术原理及概念基本概念解释技术原理介绍 Java多线程Java线程池Java异步编程Java并发控制相关技术比较实现步骤与流程准备...
    99+
    2023-10-06
    java jvm 网络
  • Micronaut框架的简单使用介绍
    目录什么是Micronaut主要特点入门依赖注入构建HTTP服务器阻塞HTTP反应式IO构建HTTP客户端声明性HTTP客户端编程HTTP客户端Micronaut客户端联合项目特征现...
    99+
    2022-11-12
  • Python的Web框架Django介绍与安装方法
    简介 Python下有许多款不同的 Web 框架。Django是重量级选手中最有代表性的一位。许多成功的网站和APP都基于Django。 Django 是一个开放源代码的 Web 应...
    99+
    2022-11-11
  • 详细介绍Golang Iris框架的安装和使用
    随着互联网的快速发展,Web开发也变得越来越重要。在现代Web开发中,一个高效、功能强大的Web框架是必不可少的。Golang Iris 就是这样一个强大的Web框架,它能够让Web开发变得更加简单、高效。本文将详细介绍Golang Iri...
    99+
    2023-05-14
  • Gin-高性能 Golang Web框架的介绍和使用
    偶遇 Gin 我之前一直在使用 Beego 框架来做应用的 Api,因为它的写法跟 PHP 的 MVC 一样,上手简单,所以对它的表现还算满意。用的久了,发现 Beego 的编程思想就是照搬了 PHP 的那一套,写法上倒没什么,但是在...
    99+
    2022-11-11
  • Java中PrintWriter使用方法介绍
    目录简介文本文件的转码复制运行程序简介 PrintWriter 与 PrintStream 相同。PrintStream 只能接字节流,而 PrintWriter 既能接字节流又能接...
    99+
    2022-11-13
  • java 非常好用的反射框架Reflections介绍
    Reflections通过扫描classpath,索引元数据,并且允许在运行时查询这些元数据。 使用Reflections可以很轻松的获取以下元数据信息: 1)获取某个类型的所有子类...
    99+
    2022-11-12
  • PHPYii2框架的关联模型使用介绍
    目录声明关联关系访问关联数据设置别名关联查询Active Record 可以将相关数据集中进来, 使其可以通过原始数据轻松访问。 例如,客户数据与订单数据相关 因为一个客户可能已经存...
    99+
    2022-11-13
  • 分布式框架Zookeeper api的使用介绍
    目录前言导入依赖建立会话创建节点获取节点数据修改节点数据删除节点前言 Zookeeper API共包含五个包,分别为: org.apache.zookeeperorg.apache....
    99+
    2022-11-13
  • Scrapy框架CrawlSpiders的介绍以及使用详解
    在Scrapy基础——Spider中,我简要地说了一下Spider类。Spider基本上能做很多事情了,但是如果你想爬取知乎或者是简书全站的话,你可能需要一个更强大的武器。CrawlSpider基于Spid...
    99+
    2022-06-04
    详解 框架 Scrapy
  • AndroidLeakCanary的使用方法介绍
    目录1.LeakCanary 如何自动初始化2.LeakCanary如何检测内存泄漏2.1LeakCanary初始化时做了什么2.2LeakCanary如何触发检测2.3LeakCa...
    99+
    2022-11-13
  • String.format()方法的使用介绍
    String.format() 方法中的 % 符号用作占位符,用于将值插入字符串中。它用于使用特定值(例如整数、浮点数或字符串)格式化字符串。% 符号后面跟着一个字母,指定要插入的值的类型,例如 %d 表示整数,%s 表示字符串。要插入的...
    99+
    2023-09-07
    java 开发语言 spring servlet
  • mysqldump的使用方法介绍
    这篇文章主要介绍了mysqldump的使用方法介绍,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。1、说明mysqldump在库被删除的情况下,无法直接从文件恢复,需要手动新建同名库,才能从文件恢复数据。2、语法shell...
    99+
    2023-06-15
  • Java接口的介绍和用法
    这篇文章主要介绍“Java接口的介绍和用法”,在日常操作中,相信很多人在Java接口的介绍和用法问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Java接口的介绍和用法”的疑惑有所帮助!接下来,请跟着小编一起来...
    99+
    2023-06-20
  • SpringMVC框架中@Controller类的方法的返回值的详细介绍
    目录 前言 1. 返回值类型为ModelAndView 2.  返回值为String(视图) 3.  返回值为void 4.  返回值为Object 5. 返回值为List  6. 返回值为String(数据) 前言 在SpringMV...
    99+
    2023-09-16
    spring java SpringMVC 返回值类型 Controller注解
  • Android图片加载框架Glide的基本用法介绍
    简介 Glide是一款图片加载框架,可以在Android平台上以简单的方式加载和展示图片。 dependencies { compile 'com.github.bump...
    99+
    2022-06-06
    glide Android
  • Vuex详细介绍和使用方法
    目录一、什么是Vuex二、运行机制三、创建项目1、使用脚手架搭建Vue项目2、安装Vuex3、启动项目4、配置使用Vuex4.1、创建store文件夹4.2、配置全局使用store对...
    99+
    2022-11-13
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作