原始对账系统,单线程
while(存在未对账订单){
????? pos = getPOrders();???// 查询未对账订单
? ????dos = getDOrders();? ?? // 查询派送单
? ????diff = check(pos, dos);? ?? // 执行对账操作
? ????save(diff);? ?? // 差异写入差异库
}
改为多线程:join
while(存在未对账订单){
? // 查询未对账订单
? Thread T1 = new Thread(()->{
? ? pos = getPOrders();
? });
? T1.start();
? // 查询派送单
? Thread T2 = new Thread(()->{
? ? dos = getDOrders();
? });
? T2.start();
? // 等待T1、T2结束
? T1.join();
? T2.join();
? diff = check(pos, dos);? ?// 执行对账操作
? save(diff);? ??// 差异写入差异库
}
创建线程耗时,用线程池优化:?CountDownLatch?
Executor executor =Executors.newFixedThreadPool(2);??// 创建2个线程的线程池
while(存在未对账订单){
? CountDownLatch latch =new CountDownLatch(2);???// 计数器初始化为2
? // 查询未对账订单
? executor.execute(()-> {
? ? pos = getPOrders();
? ? latch.countDown();
? });
? // 查询派送单
? executor.execute(()-> {
? ? dos = getDOrders();
? ? latch.countDown();
? });
? latch.await();??// 等待两个查询操作结束
? diff = check(pos, dos);???// 执行对账操作
? save(diff);??? // 差异写入差异库
}
前面我们将 getPOrders() 和 getDOrders() 这两个查询操作并行了,但这两个查询操作和对账操作 check()、save() 之间还是串行的。很显然,这两个查询操作和对账操作也是可以并行的,也就是说,在执行对账操作的时候,可以同时去执行下一轮的查询操作,这个过程可以形象化地表述为下面这幅示意图。
那接下来我们再来思考一下如何实现这步优化,两次查询操作能够和对账操作并行,对账操作还依赖查询操作的结果,这明显有点生产者 - 消费者的意思,两次查询操作是生产者,对账操作是消费者。既然是生产者 - 消费者模型,那就需要有个队列,来保存生产者生产的数据,而消费者则从这个队列消费数据。不过针对对账这个项目,我设计了两个队列,并且两个队列的元素之间还有对应关系。具体如下图所示,订单查询操作将订单查询结果插入订单队列,派送单查询操作将派送单插入派送单队列,这两个队列的元素之间是有一一对应的关系的。两个队列的好处是,对账操作可以每次从订单队列出一个元素,从派送单队列出一个元素,然后对这两个元素执行对账操作,这样数据一定不会乱掉。
下面再来看如何用双队列来实现完全的并行。一个最直接的想法是:一个线程 T1 执行订单的查询工作,一个线程 T2 执行派送单的查询工作,当线程 T1 和 T2 都各自生产完 1 条数据的时候,通知线程 T3 执行对账操作。这个想法虽看上去简单,但其实还隐藏着一个条件,那就是线程 T1 和线程 T2 的工作要步调一致,不能一个跑得太快,一个跑得太慢,只有这样才能做到各自生产完 1 条数据的时候,通知线程 T3。下面这幅图形象地描述了上面的意图:线程 T1 和线程 T2 只有都生产完 1 条数据的时候,才能一起向下执行,也就是说,线程 T1 和线程 T2 要互相等待,步调要一致;同时当线程 T1 和 T2 都生产完一条数据的时候,还要能够通知线程 T3 执行对账操作。
Vector<P> pos;? ?// 订单队列
Vector<D> dos;? ?// 派送单队列
Executor executor = Executors.newFixedThreadPool(1);??// 执行回调的线程池
final CyclicBarrier barrier = new CyclicBarrier(2, ()->{
? ????? executor.execute(()->check());? ? ?//等到barrier.await()减为0才执行
? });
void check(){
? P p = pos.remove(0);
? D d = dos.remove(0);
? diff = check(p, d);? ?// 执行对账操作
? save(diff);??? // 差异写入差异库
}
void checkAll(){
? // 循环查询订单库
? Thread T1 = new Thread(()->{
? ? while(存在未对账订单){
? ? ????? pos.add(getPOrders());??? ? ? // 查询订单库
? ? ? ????barrier.await();??? ? ? // 等待
? ? }
? });
? T1.start();?
? // 循环查询运单库
? Thread T2 = new Thread(()->{
? ? while(存在未对账订单){
? ? ????? dos.add(getDOrders());???// 查询运单库
? ? ? ????barrier.await();??? ? ? // 等待
? ? }
? });
? T2.start();
}
1.为啥要用线程池,而不是在回调函数中直接调用?
2.线程池为啥使用单线程的?
我的考虑:
1.使用线程池是为了异步操作,否则回掉函数是同步调用的,也就是本次对账操作执行完才能进行下一轮的检查。
2.线程数量固定为1,防止了多线程并发导致的数据不一致,因为订单和派送单是两个队列,只有单线程去两个队列中取消息才不会出现消息不匹配的问题。