并发
顺序执行在大多数情况下都挺好的, 简单明了, 一个时间专心做一件事, 不容易出错.
但是在多核时代, 追求更高更快更强, 应对复杂的计算和逻辑处理, 并发是不二法门.
这方面的经典书籍有两本我很喜欢
1. Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects Volume 2 Edition
by Douglas Schmidt (Author), Michael Stal (Author), Hans Rohnert (Author), Frank Buschmann (Author)
2. Java Concurrency in Practice 1st Edition
by Brian Goetz (Author), Tim Peierls (Author), Joshua Bloch (Author), Joseph Bowbeer (Author), David Holmes (Author), Doug Lea (Author)
所谓并发, 这里主要讲的是多线程编程, 当然多进程, 分布式多服务器也是并发编程的范畴, 这方面的东西以后再说
从 Java 语言的角度来看, Java 提供了基础的多线程库
Thread 早期是JVM中的一个分时调用的所谓绿色线程, 并不是真正的线程, 现在的版本多是一一对应于系统的 pthread , 线程的优先级从1到10, MAX_PRIORITY是最高优先级.
Java线程分为守护线程(thread.setDaemon(true))和非守护线程, 应该避免直接调用 thread 的 stop, suspend 和 resume , 这些方法并不可靠
Java 5 是一个分水岭, 它提供了一个非常好用的 concurrent 包, 有
- 线程执行服务 ExecutorService
- 线程安全容器 ConcurrentHashMap 等
- 阻塞队列 BlockingQueue
- 信号量 Semaphore
- 屏障 CyclicBarrier
- 倒数计量锁 CountDownLatch
- 相位器 Phaser
- 交换器 Exechanger
Java 7 提供了 Fork/Join 框架
Java 8 又是一个里程碑, 它提供了 并行流和 ComparableFuture
举例如下:
比如我们有一个图书馆系统,提供按照书名, 作者和出版日期查询的API。
现在我想查一本书, 它是本和 java 语言有关的书, 大约是2005年之后出的书,依稀记得作者是 "Brian Goetz", "Tim Peierls", "Joshua Bloch", "Joseph Bowbeer", "David Holmes" 或 "Doug Lea" 这几个人, 由于系统所提供 API 的限制,我可以按人名一个一个顺序查找,直到找到结果.
也可以并发查找,要求按照人名顺序为优先级取结果.
让我们看看具体实现和查询结果
package com.github.walterfan.example.concurrent;
import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.Uninterruptibles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* Created by walter on 15/04/2017.
*/
public class FutureTest {
public static final String AUTHOR_NAME = "Joseph Bowbeer";
private AtomicInteger counter = new AtomicInteger(0);
private Logger logger = LoggerFactory.getLogger(FutureTest.class);
private ExecutorService pool;
private String[] authors = new String[] {
"Brian Goetz",
"Tim Peierls",
"Joshua Bloch",
"Joseph Bowbeer",
"David Holmes",
"Doug Lea" };
private Instant earliestDate = Instant.parse("2005-12-31T00:00:00.00Z");
public static class Book {
final String title;
final String author;
final String isbn;
final Instant publicationDate;
public Book(String title, String author, String isbn, Instant publicationDate) {
this.title = title;
this.author = author;
this.isbn = isbn;
this.publicationDate = publicationDate;
}
@Override
public String toString() {
return "Book{" +
"title='" + title + '\'' +
", author='" + author + '\'' +
", isbn='" + isbn + '\'' +
", publicationDate=" + publicationDate +
'}';
}
}
public Book queryBook(String title, String author, Instant earliestDate) {
int num = counter.incrementAndGet();
int ms = 80;
logger.info("{}. query book, times {} " , num, author);
if("Joshua Bloch".equals(author)) {
ms = 120;
}
Uninterruptibles.sleepUninterruptibly(ms, TimeUnit.MILLISECONDS);
if(Arrays.asList(new String[]{"Brian Goetz","Tim Peierls","Joshua Bloch"}).contains(author)) {
return null;
}
return new Book("Java Concurrency in Practice", author, String.valueOf(num), Instant.parse("2006-05-19T00:00:00.00Z"));
}
@BeforeClass
public void init() {
int corePoolSize = 4;
int maxPoolSize = 16;
long keepAliveTime = 5000;
int queueCapacity = 200;
pool = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue(queueCapacity)
);
}
@AfterClass
public void clean() {
if(null == pool) {
return;
}
try {
logger.info("attempt to shutdown executor");
pool.shutdown();
pool.awaitTermination(2, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
logger.error("tasks interrupted");
}
finally {
if (!pool.isTerminated()) {
logger.error("cancel non-finished tasks");
}
pool.shutdownNow();
logger.info("shutdown finished");
}
}
public Optional querySequentially() {
for (String author : authors) {
Optional book = Optional.ofNullable(queryBook("java", author, earliestDate));
if (book.isPresent()) {
return book;
}
}
return Optional.empty();
}
public Optional queryConcurrently () {
List> futureBooks = new ArrayList<>(authors.length);
for (String author : authors) {
futureBooks.add(pool.submit(() -> queryBook("java", author, earliestDate)));
}
Optional ret = null;
for (Future futureBook : futureBooks) {
try {
ret = Optional.ofNullable(futureBook.get(100, TimeUnit.MILLISECONDS));
} catch (TimeoutException |InterruptedException|ExecutionException e) {
continue;
}
if (ret.isPresent()) {
break;
}
}
return ret;
}
public Optional queryParallelly () {
List> books = Arrays.asList(authors).parallelStream()
.map(x -> queryBook("java", x, earliestDate))
.map(x -> Optional.ofNullable(x))
.collect(Collectors.toList());
//books.stream().filter(x -> x.isPresent()).forEach(x -> logger.info(x.get().toString()));
for(String author: authors) {
Optional opt = books.stream().filter(x -> x.isPresent()).map(x -> x.get())
.filter(x -> author.equals(x.author)).findFirst();
if(opt.isPresent())
return opt;
}
return Optional.empty();
}
@Test
public void testQuery() {
logger.info("-- querySequentially ---");
long durationSequentially = recordExecutionTime(() -> querySequentially());
logger.info("-- queryConcurrently ---");
long durationConcurrently = recordExecutionTime( () -> queryConcurrently());
logger.info("-- queryParallelly ---");
long durationParallelly = recordExecutionTime(() -> queryParallelly());
logger.info("duration: querySequentially={} > queryParallelly={} > queryConcurrently={},", durationSequentially, durationParallelly, durationConcurrently );
Assert.assertTrue(durationSequentially > durationConcurrently
&& durationSequentially > durationConcurrently
&& durationParallelly > durationConcurrently);
}
public long recordExecutionTime(Supplier> supplier) {
counter.set(0);
final Stopwatch stopwatch = Stopwatch.createStarted();
Optional book = supplier.get();
stopwatch.stop();
long duration = stopwatch.elapsed(TimeUnit.MILLISECONDS);
Assert.assertEquals(book.map(x -> x.author).orElse(null), AUTHOR_NAME);
return duration;
}
}
执行结果如下:
duration: querySequentially=380 > queryParallelly=225 > queryConcurrently=137
当然是顺序执行耗时最长, queryParallelly 并行流多等待了一会儿, 所以是queryConcurrently 方法效率最高
线程池的线程数不宜过多,以免造成对于cpu和内存资源的竞争,频繁的上下文切换,也不宜过少,以免影响性能,白白闲置cpu资源,Brian Goetz 有个公式可以参考
线程数 = CPU 个数 * CPU 利用率 * (等待时间/计算时间 + 1)
IO 密集型的应用,等待时间较多,线程数可以适当增加点
而计算密集型,耗费CPU 本来就多,线程数适可而止,不宜过多
比如4核CPU, CPU 利用率为0.5(50%), 大约80%的时间在等待API 的响应
4*0.5*(80/20+1)=10 个线程
异步
异步是相对于同步来说的, 你可以一步一步以顺序执行的方式写出异步程序, 关键在于你是否会等待每一步的执行结果, 再执行下一步. 要知道, 等待是很浪费时间的, 与其苦苦等待, 不如在把用来等待的时间做点更有意义的事.
代码层面
异步其实分为多个层面, 类似上述并发所提到的, 有代码层面的异步方式, 如进程, 线程和协程.
拿最常用的写日志来说, 同步方法比较简单, 把日志写到磁盘上再返回, 最早的 log4j 也就是这种方法, 如果你需要写大量的日志, 这种方法对性能的损耗是比较大的, 毕竟写磁盘比较慢, 即使是追加的方式也不可忽视.
Log4j 中使用异步log , 会提升 6~68倍的吞吐量, Log4j1 中的 Async appender 使用 ArrayBlockingQueue 和单独的 log 线程来实现异步, 应用线程把日志写到 ArrayBlockingQueue 即返回, 而单独的 log 线程把日志保存到日志文件中, log4j2 中使用了一个无锁化的线程间通信库 LMAX Disruptor , 减少了锁等待时间, 据说性能又有了大幅提升
上面提到的 Future 和 ComparableFuture 是常用的方法, 其实 future, promise, delay, 和 deferred 都是在一些编程语言中表示在并发执行中的未知结果的代理对象, 因为实际的结果可能尚未执行和计算得出, 可以从这个代理对象中查询到执行的结果.
- Future: 表示一个异步计算的结果
- ComparableFuture: 一个可以显式完成的异步计算结果, 有一个 CompletionStage , 支持在完成时触发相应的函数和动作, 也就是我们常说的 Promise
示例如下
package com.github.walterfan.hellotest;
import com.google.common.util.concurrent.Uninterruptibles;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
@Slf4j
@Data
class Caller {
private String phoneNumber;
public Executor executor = Executors.newFixedThreadPool(10);
public void onSuccess(String resp) {
log.info("call success, resp is {}", resp);
}
public Void onError(Throwable exp) {
log.info("error: ", exp);
return null;
}
public void writeMetrics(Void nothing) {
log.info("metrics: sent call request");
}
public String sendReqeust() {
log.info("send request to call {}", phoneNumber);
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
return "success";
}
}
@Slf4j
public class CompletableFutureTest {
@Test
public void testFuture() {
Caller caller = new Caller();
caller.setPhoneNumber("13011166888");
CompletableFuture promise = CompletableFuture.supplyAsync(caller::sendReqeust);
promise.thenAccept(strResp -> caller.onSuccess(strResp))
.exceptionally(exp -> caller.onError(exp) )
.thenAccept(caller::writeMetrics);
log.info("need not wait here, just for test to sleep 2 seconds");
Uninterruptibles.sleepUninterruptibly(2, TimeUnit.SECONDS);
}
}
API 层面
在 API 层面, 相比传统的请求-响应的模式, 不是等待任务完成才回复响应 200 OK, 而是立即响应 202 Accept, 之后在任务进行过程中或完成之后发送若干通知, 这样客户端和服务器端都无须浪费时间等待, 例如以下流程
Client->Server: POST /tasks (notifyUrl)
Server-->Client: 202 Accept (taskId)
Server->Client: POST notifyUrl(started)
Client-->Server: 200 OK
opt client query server
Client->Server: GET /tasks/taskId
Server-->Client: 200 OK (in-progress)
end
Server->Client: POST notifyUrl(finished)
Client-->Server: 200 OK
这里的 Client 和 Server 也可以是 Service1 和 Service2, 如果 Client 是Web App, 那么也可以由 Client 主动以 taskId 来查询, 抑或以 websocket , BOSH(Bidirectional-streams Over Synchronous HTTP) 之类的双工通信方式进行主动通知.
Webhook
Webhook 的做法也是异步的一种方式, 它是一种用户定义的 HTTP 回调, 它和我们在写代码中设置一个回调函数的方式是一样一样的, 在发送请求和回复响应时都可以给定一个预定义的URL, 按照约定的调用方式在某种条件下或某个事件发生时触发这个 Webhook 回调.
实际应用中, 既可以用同步的方式使用它, 比如在持续集成系统 Jenkins 中我们可以定义一些 webhook, 例如在关键步骤时需要负责人批准, 我们可以定义一个 webhook , 在执行下一步关键步骤时调用此 webhook, 通过 IM 向负责人发送一个 url, 负责人点击这个 url 会出现一个表单, 包含同意和拒绝两个按钮, 点击同意则回调 Jenkins 继续下一步, 否则停止执行下一步.
Github 的 Pull Request 也算是用异步的方式在某人创建 PR 时发送邮件来通知相关者来审查代码, 而创建 PR 者不会等待这个异步的通知
Pubsub
订阅/发布( subscribe/publish) 模式也是如此, 这个模式如此简单又如此有用, 这里的异步体现在生产者不等消费者来取走产品, 把生产出的内容消息发到消息队列服务器 (MQ Broker) 上就好了, 而消费者也不会等着生产者, 只需订阅关心的主题, 所订阅的内容会自动推送给我
银行的排号机是一个程序员可以借鉴的一个非常好的参照物, 它既是一个发布订阅系统, 也是一个流量控制系统, 有关发布订阅模式与消息队列系统, 内容很多, 这里不做赘述, 另外写字详细阐述.