我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java 8 异步编程 CompletableFuture 全解析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Java 8 异步编程 CompletableFuture 全解析

本文转载自微信公众号「KK架构师」,作者wangkai 。转载本文请联系KK架构师公众号。   

本文大纲速看

 

一、异步编程

通常来说,程序都是顺序执行,同一时刻只会发生一件事情。如果一个函数依赖于另一个函数的结果,它只能等待那个函数结束才能继续执行,从用户角度来说,整个程序才算执行完毕。但现在的计算机普遍拥有多核 CPU,在那里干等着毫无意义,完全可以在另一个处理器内核上干其他工作,耗时长的任务结束之后会主动通知你。这就是异步编程的出发点:充分使用多核 CPU 的优势,最大程度提高程序性能。一句话来说:所谓异步编程,就是实现一个无需等待被调用函数的返回值而让操作继续运行的方法。

二、抛出一个问题:如何实现烧水泡茶的程序

 

最后我们会使用传统方式和 Java8 异步编程方式分别实现,来对比一下实现复杂度。

三、Java5 的 Future 实现的异步编程

Future 是 Java 5 添加的类,用来描述一个异步计算的结果。你可以使用 isDone() 方法检查计算是否完成,或者使用 get() 方法阻塞住调用线程,直到计算完成返回结果,也可以使用 cancel() 方法停止任务的执行。

  1. public static void main(String[] args) throws InterruptedException, ExecutionException { 
  2.         ExecutorService es = Executors.newFixedThreadPool(5); 
  3.         Future<Integer> f = es.submit(() -> 100); 
  4.         System.out.println(f.get()); 
  5.         es.shutdown(); 
  6.     } 

虽然 Future 提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方式显然和我们异步编程的初衷相违背,轮询的方式又会耗费无谓的 CPU 资源,而且也不能及时的获取结果。

当然,很多其他的语言采用回调的方式来实现异步编程,比如 Node.js;Java 的一些框架,比如 Netty,Google Guava 也扩展了 Future 接口,提供了很多回调的机制,封装了工具类,辅助异步编程开发。

Java 作为老牌编程语言,自然也不会落伍。在 Java 8 中,新增了一个包含 50 多个方法的类:CompletableFuture,提供了非常强大的 Future 扩展功能,可以帮助我们简化异步编程的复杂性,提供函数式编程的能力。

四、CompletableFuture 类功能概览

如下图是 CompletableFuture 实现的接口:

 

 

它实现了 Future 接口,拥有 Future 所有的特性,比如可以使用 get() 方法获取返回值等;还实现了 CompletionStage 接口,这个接口有超过 40 个方法,功能太丰富了,它主要是为了编排任务的工作流。

我们可以把工作流和工作流之间的关系分类为三种:串行关系,并行关系,汇聚关系。

串行关系

 

提供了如下的 api 来实现(先大致浏览一遍):

  1. CompletionStage thenApply(fn); 
  2. CompletionStage thenApplyAsync(fn); 
  3. CompletionStage thenAccept(consumer); 
  4. CompletionStage thenAcceptAsync(consumer); 
  5. CompletionStage thenRun(action); 
  6. CompletionStage thenRunAsync(action); 
  7. CompletionStage thenCompose(fn); 
  8. CompletionStage thenComposeAsync(fn); 

并行关系

 

 

多线程异步执行就是并行关系

汇聚关系

 

汇聚关系,又分为 AND 汇聚关系和 OR 汇聚关系:

AND 汇聚关系,就是所有依赖的任务都完成之后再执行;OR 汇聚关系,就是依赖的任务中有一个执行完成,就开始执行。

AND 汇聚关系由这些接口表达:

  1. CompletionStage thenCombine(other, fn); 
  2. CompletionStage thenCombineAsync(other, fn); 
  3. CompletionStage thenAcceptBoth(other, consumer); 
  4. CompletionStage thenAcceptBothAsync(other, consumer); 
  5. CompletionStage runAfterBoth(other, action); 
  6. CompletionStage runAfterBothAsync(other, action); 

OR 汇聚关系由这些接口来表达:

  1. CompletionStage applyToEither(other, fn); 
  2. CompletionStage applyToEitherAsync(other, fn); 
  3. CompletionStage acceptEither(other, consumer); 
  4. CompletionStage acceptEitherAsync(other, consumer); 
  5. CompletionStage runAfterEither(other, action); 
  6. CompletionStage runAfterEitherAsync(other, action); 

五、CompletableFuture 接口精讲

1、提交执行的静态方法

方法名描述

方法名 描述
runAsync(Runnable runnable) 执行异步代码,使用 ForkJoinPool.commonPool() 作为它的线程池
runAsync(Runnable runnable, Executor executor) 执行异步代码,使用指定的线程池
supplyAsync(Supplier supplier) 异步执行代码,有返回值,使用 ForkJoinPool.commonPool() 作为它的线程池
supplyAsync(Supplier supplier, Executor executor) 异步执行代码,有返回值,使用指定的线程池执行

上述四个方法,都是提交任务的,runAsync 方法需要传入一个实现了 Runnable 接口的方法,supplyAsync 需要传入一个实现了 Supplier 接口的方法,实现 get 方法,返回一个值。

(1)run 和 supply 的区别

run 就是执行一个方法,没有返回值,supply 执行一个方法,有返回值。

(2)一个参数和两个参数的区别

第二个参数是线程池,如果没有传,则使用自带的 ForkJoinPool.commonPool() 作为线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)

2、串行关系 api

这些 api 之间主要是能否获得前一个任务的返回值与自己是否有返回值的区别。

api 是否可获得前一个任务的返回值 是否有返回值
thenApply
thenAccept
thenRun 不能
thenCompose

(1) thenApply 和 thenApplyAsync 使用

thenApply 和 thenApplyAsync 把两个并行的任务串行化,另一个任务在获得上一个任务的返回值之后,做一些加工和转换。它也是有返回值的。

  1. public class BasicFuture4 { 
  2.  
  3.     @Data 
  4.     @AllArgsConstructor 
  5.     @ToString 
  6.     static class Student { 
  7.         private String name
  8.     } 
  9.      
  10.     public static void main(String[] args) throws ExecutionException, InterruptedException { 
  11.         CompletableFuture future = CompletableFuture.supplyAsync(() -> "Jack"
  12.                 .thenApply(s -> s + " Smith"
  13.                 .thenApply(String::toUpperCase) 
  14.                 .thenApplyAsync(Student::new); 
  15.         System.out.println(future.get()); 
  16.     } 
  17.  

结果可以看到,输入是一个字符串,拼接了一个字符串,转换成大写,new 了一个 Student 对象返回。

  1. BasicFuture4.Student(name=JACK SMITH) 

和 thenApply 一起的还有 thenAccept 和 thenRun,thenAccept 能获得到前一个任务的返回值,但是自身没有返回值;thenRun 不能获得前一个任务的返回值,自身也没有返回值。

(2)thenApply 和 thenApplyAsync 的区别

这两个方法的区别,在于谁去执行任务。如果使用 thenApplyAsync,那么执行的线程是从 ForkJoinPool.commonPool() 或者自己定义的线程池中取线程去执行。如果使用 thenApply,又分两种情况,如果 supplyAsync 方法执行速度特别快,那么 thenApply 任务就使用主线程执行,如果 supplyAsync 执行速度特别慢,就是和 supplyAsync 执行线程一样。

可以使用下面的例子演示一下:

  1. package com.dsj361.future; 
  2.  
  3. import java.util.concurrent.CompletableFuture; 
  4. import java.util.concurrent.ExecutionException; 
  5.  
  6.  
  7. public class BasicFuture8 { 
  8.  
  9.     public static void main(String[] args) throws ExecutionException, InterruptedException { 
  10.         System.out.println("----------supplyAsync 执行很快"); 
  11.         CompletableFuture future1 = CompletableFuture.supplyAsync(() -> { 
  12.             System.out.println(Thread.currentThread().getName()); 
  13.             return "1"
  14.         }).thenApply(s -> { 
  15.             System.out.println(Thread.currentThread().getName()); 
  16.             return "2"
  17.         }); 
  18.         System.out.println(future1.get()); 
  19.  
  20.         System.out.println("----------supplyAsync 执行很慢"); 
  21.         CompletableFuture future2 = CompletableFuture.supplyAsync(() -> { 
  22.             try { 
  23.                 Thread.sleep(1000); 
  24.             } catch (InterruptedException e) { 
  25.             } 
  26.             System.out.println(Thread.currentThread().getName()); 
  27.             return "1"
  28.         }).thenApply(s -> { 
  29.             System.out.println(Thread.currentThread().getName()); 
  30.             return "2"
  31.         }); 
  32.         System.out.println(future2.get()); 
  33.     } 

执行结果:

  1. ----------supplyAsync 执行很快 
  2. ForkJoinPool.commonPool-worker-1 
  3. main 
  4. ----------supplyAsync 执行很慢 
  5. ForkJoinPool.commonPool-worker-1 
  6. ForkJoinPool.commonPool-worker-1 

(3)thenCompose 的使用

假设有两个异步任务,第二个任务想要获取第一个任务的返回值,并且做运算,我们可以用 thenCompose。此时使用 thenApply 也可以实现,看一段代码发现他们的区别:

  1. public class BasicFuture9 { 
  2.  
  3.     public static void main(String[] args) throws ExecutionException, InterruptedException { 
  4.         CompletableFuture future = getLastOne().thenCompose(BasicFuture9::getLastTwo); 
  5.         System.out.println(future.get()); 
  6.  
  7.         CompletableFuture> future2 = getLastOne().thenApply(s -> getLastTwo(s)); 
  8.         System.out.println(future2.get().get()); 
  9.     } 
  10.  
  11.     public static CompletableFuture getLastOne(){ 
  12.         return CompletableFuture.supplyAsync(()-> "topOne"); 
  13.     } 
  14.  
  15.     public static CompletableFuture getLastTwo(String s){ 
  16.         return CompletableFuture.supplyAsync(()-> s + "  topTwo"); 
  17.     } 

可以看到使用 thenApply 的时候,需要使用两个 get() 方法才能获取到最终的返回值,使用 thenCompose 只要一个即可。

3、And 汇聚关系 Api

(1)thenCombine 的使用

加入我们要计算两个异步方法返回值的和,就必须要等到两个异步任务都计算完才能求和,此时可以用 thenCombine 来完成。

  1. public static void main(String[] args) throws ExecutionException, InterruptedException { 
  2.     CompletableFuture<Integer> thenComposeOne = CompletableFuture.supplyAsync(() -> 192); 
  3.     CompletableFuture<Integer> thenComposeTwo = CompletableFuture.supplyAsync(() -> 196); 
  4.     CompletableFuture<Integer> thenComposeCount = thenComposeOne 
  5.         .thenCombine(thenComposeTwo, (s, y) -> s + y); 
  6.  
  7.     thenComposeOne.thenAcceptBoth(thenComposeTwo,(s,y)-> System.out.println("thenAcceptBoth")); 
  8.     thenComposeOne.runAfterBoth(thenComposeTwo, () -> System.out.println("runAfterBoth")); 
  9.  
  10.     System.out.println(thenComposeCount.get()); 

可以看到 thenCombine 第二个参数是一个 Function 函数,前面两个异步任务都完成之后,使用这个函数来完成一些运算。

(2)thenAcceptBoth

接收前面两个异步任务的结果,执行一个回调函数,但是这个回调函数没有返回值。

(3)runAfterBoth

接收前面两个异步任务的结果,但是回调函数,不接收参数,也不返回值。

4、Or 汇聚关系 Api

  1. public class BasicFuture11 { 
  2.  
  3.     public static void main(String[] args) throws ExecutionException, InterruptedException { 
  4.         CompletableFuture<Integer> thenComposeOne = CompletableFuture.supplyAsync(() -> 192); 
  5.         CompletableFuture<Integer> thenComposeTwo = CompletableFuture.supplyAsync(() -> 196); 
  6.         CompletableFuture<Integer> thenComposeCount = thenComposeOne 
  7.                 .applyToEither(thenComposeTwo, s -> s + 1); 
  8.  
  9.         thenComposeOne.acceptEither(thenComposeTwo,s -> {}); 
  10.          
  11.         thenComposeOne.runAfterEither(thenComposeTwo,()->{}); 
  12.  
  13.         System.out.println(thenComposeCount.get()); 
  14.     } 

(1)applyToEither

任何一个执行完就执行回调方法,回调方法接收一个参数,有返回值

(2)acceptEither

任何一个执行完就执行回调方法,回调方法接收一个参数,无返回值

(3)runAfterEither

任何一个执行完就执行回调方法,回调方法不接收参数,也无返回值

5、处理异常

上面我们讲了如何把几个异步任务编排起来,执行一些串行或者汇聚操作。还有一个重要的地方,就是异常的处理。

先看下面的例子:

  1. public static void main(String[] args) throws ExecutionException, InterruptedException { 
  2.     CompletableFuture.supplyAsync(() -> { 
  3.         System.out.println("execute one "); 
  4.         return 100; 
  5.     }) 
  6.         .thenApply(s -> 10 / 0) 
  7.         .thenRun(() -> System.out.println("thenRun")) 
  8.         .thenAccept(s -> System.out.println("thenAccept")); 
  9.  
  10.     CompletableFuture.runAsync(() -> System.out.println("other")); 

结果:

  1. execute one  
  2. other 

可以发现,只要链条上有一个任务发生了异常,这个链条下面的任务都不再执行了。

但是 main 方法上的接下来的代码还是会执行的。

所以这个时候,需要合理的去处理异常来完成一些收尾的工作。

  1. public class BasicFuture12 { 
  2.  
  3.     public static void main(String[] args) throws ExecutionException, InterruptedException { 
  4.         CompletableFuture.supplyAsync(() -> { 
  5.             System.out.println("execute one "); 
  6.             return 100; 
  7.         }) 
  8.                 .thenApply(s -> 10 / 0) 
  9.                 .thenRun(() -> System.out.println("thenRun")) 
  10.                 .thenAccept(s -> System.out.println("thenAccept")) 
  11.                 .exceptionally(s -> { 
  12.                     System.out.println("异常处理"); 
  13.                     return null
  14.                 }); 
  15.  
  16.         CompletableFuture.runAsync(() -> System.out.println("other")); 
  17.     } 

可以使用 exceptionally 来处理异常。

使用 handle() 方法也可以处理异常。但是 handle() 方法的不同之处在于,即使没有发生异常,也会执行。

六、烧水泡茶程序的实现

1、使用 Thread 多线程和 CountDownLatch 来实现

  1. public class MakeTee { 
  2.  
  3.     private static CountDownLatch countDownLatch = new CountDownLatch(2); 
  4.  
  5.     static class HeatUpWater implements Runnable { 
  6.  
  7.         private CountDownLatch countDownLatch; 
  8.  
  9.         public HeatUpWater(CountDownLatch countDownLatch) { 
  10.             this.countDownLatch = countDownLatch; 
  11.         } 
  12.         @Override 
  13.         public void run() { 
  14.             try { 
  15.                 System.out.println("洗水壶"); 
  16.                 Thread.sleep(1000); 
  17.                 System.out.println("烧开水"); 
  18.                 Thread.sleep(5000); 
  19.                 countDownLatch.countDown(); 
  20.             } catch (InterruptedException e) { 
  21.             } 
  22.  
  23.         } 
  24.     } 
  25.  
  26.     static class PrepareTee implements Runnable { 
  27.         private CountDownLatch countDownLatch; 
  28.  
  29.         public PrepareTee(CountDownLatch countDownLatch) { 
  30.             this.countDownLatch = countDownLatch; 
  31.         } 
  32.  
  33.         @Override 
  34.         public void run() { 
  35.             try { 
  36.                 System.out.println("洗茶壶"); 
  37.                 Thread.sleep(1000); 
  38.                 System.out.println("洗茶杯"); 
  39.                 Thread.sleep(1000); 
  40.                 System.out.println("拿茶叶"); 
  41.                 Thread.sleep(1000); 
  42.                 countDownLatch.countDown(); 
  43.             } catch (InterruptedException e) { 
  44.             } 
  45.         } 
  46.     } 
  47.     public static void main(String[] args) throws InterruptedException { 
  48.         new Thread(new HeatUpWater(countDownLatch) ).start(); 
  49.         new Thread(new PrepareTee(countDownLatch)).start(); 
  50.         countDownLatch.await(); 
  51.         System.out.println("准备就绪,开始泡茶"); 
  52.     } 

这里我们使用两个线程,分别执行烧水和泡茶的程序,使用 CountDownLatch 来协调两个线程的进度,等到他们都执行完成之后,再执行泡茶的动作。

可以看到这种方法,多了很多不必要的代码,new Thread,人工维护 CountDownLatch 的进度。

2、使用 CompletableFuture 来实现

  1. public class MakeTeeFuture { 
  2.  
  3.     public static void main(String[] args) throws ExecutionException, InterruptedException { 
  4.         CompletableFuture future1 = CompletableFuture.runAsync(() -> { 
  5.             try { 
  6.                 System.out.println("洗水壶"); 
  7.                 Thread.sleep(1000); 
  8.                 System.out.println("烧开水"); 
  9.                 Thread.sleep(5000); 
  10.             } catch (InterruptedException e) { 
  11.                 e.printStackTrace(); 
  12.             } 
  13.         }); 
  14.         CompletableFuture future2 = CompletableFuture.runAsync(() -> { 
  15.             try { 
  16.                 System.out.println("洗茶壶"); 
  17.                 Thread.sleep(1000); 
  18.                 System.out.println("洗茶杯"); 
  19.                 Thread.sleep(1000); 
  20.                 System.out.println("拿茶叶"); 
  21.                 Thread.sleep(1000); 
  22.             } catch (InterruptedException e) { 
  23.                 e.printStackTrace(); 
  24.             } 
  25.         }); 
  26.         CompletableFuture finish = future1.runAfterBoth(future2, () -> { 
  27.             System.out.println("准备完毕,开始泡茶"); 
  28.         }); 
  29.         System.out.println(finish.get()); 
  30.     } 

这个程序极度简单,无需手工维护线程,给任务分配线程的工作也不需要关注。

同时语义也更加清晰,future1.runAfterBoth(future2,......) 能够清晰的表述“任务 3 要等到任务 1 和任务 2 都完成之后才能继续开始”

然后代码更加简练并且专注于业务逻辑,几乎所有的代码都是业务逻辑相关的。

七、总结

本文介绍了异步编程的概念,以及 Java8 的 CompletableFuture 是如何优雅的处理多个异步任务之间的协调工作的。CompletableFuture 能够极大简化我们对于异步任务编排的工作,Flink 在提交任务时,也是使用这种异步任务的方式,去编排提交时和提交后对于任务状态处理的一些工作的。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java 8 异步编程 CompletableFuture 全解析

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

下载Word文档

猜你喜欢

Java 8 异步编程 CompletableFuture 全解析

Future 是 Java 5 添加的类,用来描述一个异步计算的结果。你可以使用 isDone() 方法检查计算是否完成,或者使用 get() 方法阻塞住调用线程,直到计算完成返回结果,也可以使用 cancel() 方法停止任务的执行。

如何理解Java 8异步编程CompletableFuture

本篇内容介绍了“如何理解Java 8异步编程CompletableFuture”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!本文大纲速看一、
2023-06-15

异步编程利器:CompletableFuture详解

最近刚好使用CompeletableFuture优化了项目中的代码,所以跟大家一起学习CompletableFuture。

并发编程 | 从Future到CompletableFuture - 简化 Java 中的异步编程

引言 在并发编程中,我们经常需要处理多线程的任务,这些任务往往具有依赖性,异步性,且需要在所有任务完成后获取结果。Java 8 引入了 CompletableFuture 类,它带来了一种新的编程模式,让我们能够以函数式编程的方式处理并发任
2023-08-19

CompletableFuture异步编程中的异常处理陷阱与解决方案

在CompletableFuture异步编程中,异常处理是一个需要重点关注的问题。通过合理使用whenComplete、exceptionally和handle方法,并保留堆栈追踪信息,我们可以有效地处理异步任务中的异常,提高程序的稳定性和

如何分析Java中的异步网络编程

如何分析Java中的异步网络编程,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。目前java平台已经广泛应用于各类客户/服务器系统中,在实际编程中,往往需要网络的
2023-06-03

Java异步编程之Callbacks与Futures模型详解

这篇文章主要为大家详细介绍了Java异步编程中Callbacks与Futures模型的使用,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
2023-03-24

Java异步编程中如何进行FutureTask源码分析

本篇文章给大家分享的是有关Java异步编程中如何进行FutureTask源码分析,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。Java的异步编程是一项非常常用的多线程技术。但之
2023-06-19

Java编程cas操作全面解析

CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等
2023-05-31

如何解析异步编程In .NET APM/EAP和async/await

如何解析异步编程In .NET APM/EAP和async/await,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。概述在之前写的一篇关于async和await的前世今生的文章
2023-06-17

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录