Java8中的异步编程,Guava Cache教程
所谓异步调用其实就是指调用一个方法(函数)后调用者可以无需等待其返回结果,调用者代码可以直接继续运行。比如在 Java 中,我们可以通过启动一个线程的方式来完成方法调用中的部分计算逻辑,而调用者本身无需同步等待返回结果,可以继续运行或者直接返回。但是某些场景下调用者仍需要获取异步线程的计算结果。此时我们会想到 JDK5 新增的 Future 接口。Future,顾名思义,表示未来(将来时),即用于描述一个异步计算的结果。
虽然说 Future 提供了异步执行任务的能力,但是对于异步计算结果的获取却只能通过阻塞或者主动轮询的方式得到。阻塞的方式显然违背了异步编程的初衷;而轮询的方式又会浪费 CPU 资源,并且不能及时获取到计算结果。能不能让执行异步计算的线程主动向主线程通知其进度,以及成功失败与否呢?这就是本文要介绍的 JDK8引入的 CompletableFuture。
在 JDK8 中引入的CompletableFuture提供了非常强大的Future的扩展功能,降低了异步编程、并发编程的复杂度,并且提供了函数式编程的能力。相比 Future 的一个明显的优势是支持异步回调,即可以传入回调函数。当异步计算任务完成或者发生异常时,会主动调用回调对象的回调方法,这就省去了我们主动等待、获取异步计算结果,后者可以认为是一种伪异步模式。CompletableFuture 还提供了多种转换和合并 CompletableFuture 的方法,即可以实现异步任务链、任务组合。CompletableFuture的类图结构如下图。CompletableFuture内部持有一个Executor类型的对象asyncPool,作为执行异步任务的线程池。
创建异步任务
使用线程池的方式
在本公众号的文章《 Java线程池核心原理与最佳实践》中,我们学习了线程池的知识,使用线程池、多线程的方式就能实现异步效果。
1)executor.submit带返回值
@Test public void test1() throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<String> future = executor.submit(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } if (false) { throw new RuntimeException("some error"); } else { System.out.println(getNowTime() + Thread.currentThread() + " task end"); return "Success"; } }); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); //阻塞等待任务执行完成,如果已完成则直接返回结果;如果执行任务异常,则get方法会重新抛出捕获的异常 System.out.println("task result:" + future.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); executor.shutdown(); }
2)executor.submit不带返回值
@Test public void test2() throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<?> future = executor.submit(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task end"); }); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); System.out.println("task result:" + future.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); executor.shutdown(); }
3)executor.execute不带返回值
@Test public void test3() throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } if (true) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task end"); }); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); Thread.sleep(3000); executor.shutdown(); }
使用CompletableFuture
CompletableFuture 的supplyAsync
表示创建带返回值的异步任务的,功能类似于ExecutorService
的submit(Callable<T> task)
方法;runAsync
表示创建不带返回值的异步任务,类似于ExecutorService
的submit(Runnable task)
方法。
1)创建带返回值的异步任务(supplyAsync)
@Test public void testSupplyAsync() throws Exception { CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task end"); return 100; }); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); //阻塞等待任务执行完成,如果已完成则直接返回结果;如果执行任务异常,则get方法会重新抛出捕获的异常 System.out.println("task result:" + future.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
2)创建不带返回值的异步任务(runAsync)
@Test public void testRunAsync() throws Exception { CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task end"); }); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); //没有返回值的情况下返回null System.out.println("task result:" + future.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
所谓异步任务,归根到底都是要以线程方式执行。因此上述2个方法各有一个重载版本,可以指定执行异步任务的Executor实现。如果不指定,默认使用ForkJoinPool.commonPool()
,如果当前机器是单核的,则默认使用ThreadPerTaskExecutor
,该类是一个内部类,每次执行任务都会创建一个新线程。
@Test public void testRunAsyncWithPool() throws Exception { //手动创建线程池 ExecutorService pool = Executors.newSingleThreadExecutor(); CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task end"); }, pool);//手动指定线程池 System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); //没有返回值的情况下返回null System.out.println("task result:" + future.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
@Test public void testSupplyAsyncWithPool() throws Exception { ForkJoinPool pool = new ForkJoinPool(); //创建线程池 CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task end"); return 100; }, pool);//指定线程池 System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); //阻塞等待任务执行完成,如果已完成则直接返回结果;如果执行任务异常,则get方法会重新抛出捕获的异常 System.out.println("task result:" + future.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
异步回调
CompletableFuture
相比传统 Future
的一个明显的优势是可以传入回调函数,不需要我们手动get阻塞等待,获得结果后再进行下一步处理;而是可以传入回调函数,等任务执行完成后自动调用回调函数。回调函数有很多种,下面一一讲解。
thenAccept会接收上一个任务的返回值作为参数,但是无返回值;thenRun的方法没有入参,也没有返回值。thenApply会接收上一个任务的返回值作为参数,并且有返回值。
thenAccept/thenRun
@Test public void testThenAcceptAndThenRun() throws Exception { CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {//执行有返回值的异步任务 System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }); CompletableFuture<Void> future2 = future1.thenAccept(res1 -> {//上一个任务的执行结果作为入参,无返回值 System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("res1=" + res1); System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); }); CompletableFuture<Void> future3 = future2.thenRun(() -> {//无入参,也无返回值 System.out.println(getNowTime() + Thread.currentThread() + " task3 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getNowTime() + Thread.currentThread() + " task3 end"); }); Thread.sleep(200); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task1!"); //阻塞等待任务执行完成,如果已完成则直接返回结果;如果执行任务异常,则get方法会重新抛出捕获的异常 System.out.println("task1 result:" + future1.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task2!"); System.out.println("task2 result:" + future2.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task3!"); System.out.println("task3 result:" + future3.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
thenApply/thenApplyAsync
thenApply表示某个任务执行完成后执行的动作,即回调方法,会将该任务的执行结果即方法返回值作为入参传递到回调方法中。
thenApplyAsync
与thenApply
的区别是前者将回调方法放到线程池中运行。那么执行这两个任务的就可能是线程池中的两个不同的线程。thenApplyAsync
有一个重载版本,可以指定执行异步任务的Executor
实现,如果不指定,默认使用ForkJoinPool.commonPool()
。
@Test public void testThenApply() throws Exception { ForkJoinPool pool = new ForkJoinPool(); CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }, pool); //CompletableFuture<Integer> future2 = future1.thenApplyAsync(res1 -> { CompletableFuture<Integer> future2 = future1.thenApply(res1 -> { System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); return res1 + 99;//可以获得上一个任务的返回值,并且返回另一个值 }); Thread.sleep(200); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task1!"); //阻塞等待任务执行完成,如果已完成则直接返回结果;如果执行任务异常,则get方法会重新抛出捕获的异常 System.out.println("task1 result:" + future1.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task2!"); System.out.println("task2 result:" + future2.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
whenComplete
whenComplete
是当某个任务执行完成后执行的回调方法,会将执行结果或者执行期间抛出的异常传递给回调方法。如果是正常执行则没有异常,传给回调方法的异常参数为null。回调方法对应的CompletableFuture的结果和原任务一致,如果该任务正常执行,则get方法返回执行结果;如果是执行异常,则get方法抛出异常。
@Test public void testWhenComplete() throws Exception { CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {//执行有返回值的异步任务 System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (true) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }); //上一个任务执行完成后会将执行结果和执行过程中抛出的异常传入回调方法,如果是正常执行的则传入的异常为null CompletableFuture<Integer> future2 = future1.whenComplete((r, e) -> { System.out.println("future1 res=" + r); System.out.println("exception=" + e); System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); try { Thread.sleep(2000); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); }); Thread.sleep(3000); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task1!"); //如果future1是正常执行的,future2.get的结果就是future1执行的结果 //如果future1是执行异常,则future1.get()或future2.get()都会抛出异常 System.out.println("task1 result:" + future1.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task2!"); System.out.println("task2 result:" + future2.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
handle
和whenComplete
基本一致。区别在于handle的回调方法有返回值,且handle
方法返回的CompletableFuture的结果是回调方法的执行结果或者回调方法执行期间抛出的异常,而不是原CompletableFuture的结果。
@Test public void testHandle() throws Exception { CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {//执行有返回值的异步任务 System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (true) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }); //上一个任务执行完成后会将执行结果和执行过程中抛出的异常传入回调方法,如果是正常执行的则传入的异常为null CompletableFuture<Integer> future2 = future1.handle((r, e) -> { System.out.println("future1 res=" + r); System.out.println("exception=" + e); System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); try { Thread.sleep(2000); } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); return 99;//有返回值 }); Thread.sleep(3000); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task1!"); //如果future1是执行异常,则future1.get会抛出异常 //future2.get的结果是future2的返回值,跟future1没关系了,这就是handle和whenComplete的区别 System.out.println("task1 result:" + future1.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task2!"); System.out.println("task2 result:" + future2.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
exceptionally
exceptionally
指定某个任务执行异常时执行的回调方法,会将抛出异常作为参数传递到回调方法中。如果该任务正常执行则不会进入exceptionally
方法,否则进入exceptionally
方法。这比我们使用try{...}catch(){...}
在任务里手动处理异常灵活得多。
@Test public void testExceptionally() throws Exception { CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {//执行有返回值的异步任务 System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (true) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }); CompletableFuture<Integer> future2 = future1.thenApply(res1 -> {//上一个任务执行异常则不调用此逻辑,执行异常时,将抛出的异常作为入参传递给回调方法 System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("res1=" + res1); System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); return 1; }).exceptionally((param) -> { System.out.println("error:" + param); param.printStackTrace(); return 0; }); Thread.sleep(200); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task1!"); //阻塞等待任务执行完成,如果已完成则直接返回结果;如果执行任务异常,则get方法会重新抛出捕获的异常 System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task2!"); //task2的结果会随着task1有无异常而不同 System.out.println("task2 result:" + future2.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
异步组合
CompletableFuture
允许将多个Future
合并成一个Future
,也允许多个独立的CompletableFuture
同时并行执行。
thenCombine/thenAcceptBoth/runAfterBoth
这3个方法都是将两个CompletableFuture组合起来。被组合的任务都正常执行完了才会继续执行下一个任务。
thenCombine
将两个任务的执行结果作为方法入参传递到指定方法中,且该方法有返回值;
thenAcceptBoth
同样将两个任务的执行结果作为方法入参,但是无返回值;
runAfterBoth
既没有入参,也没有返回值。
注意两个任务中只要有一个执行异常,则将该异常信息作为指定任务的执行结果。
@Test public void testCombine() throws Exception { System.out.println(getNowTime() + Thread.currentThread() + " main thread start!"); CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }); CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); return 88; }); //thenCombine将两个任务的执行结果分别作为方法入参传递到指定方法中,且该方法有返回值 //task3要等到其组合的task1和task2执行完成后才会执行 CompletableFuture<Integer> future3 = future1.thenCombine(future2, (res1, res2) -> { System.out.println(getNowTime() + Thread.currentThread() + " task3 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task3 end"); return res1 + res2; }); //thenAcceptBoth也是将两个任务的执行结果作为方法入参,但是无返回值 //task4要等到其组合的task1和task2执行完成后才会执行 CompletableFuture<Void> future4 = future1.thenAcceptBoth(future2, (res1, res2) -> { System.out.println(getNowTime() + Thread.currentThread() + " task4 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("res1=" + res1 + ",res2=" + res2); System.out.println(getNowTime() + Thread.currentThread() + " task4 end"); }); //runAfterBoth既没有入参,也没有返回值 //task5要等到其组合的task1和task2执行完成后才会执行 CompletableFuture<Void> future5 = future2.runAfterBoth(future1, () -> { System.out.println(getNowTime() + Thread.currentThread() + " task5 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getNowTime() + Thread.currentThread() + " task5 end"); }); //主线程在没有调用CompletableFuture.get()之前都不会阻塞 System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task1!"); System.out.println("task1 result:" + future1.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task2!"); System.out.println("task2 result:" + future2.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task3!"); System.out.println("task3 result:" + future3.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task4!"); System.out.println("task4 result:" + future4.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task5!"); System.out.println("task5 result:" + future5.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
thenCompose
thenCompose
会在某个任务执行完成后,将该任务的执行结果作为方法入参然传入指定的方法。该方法会返回一个新的CompletableFuture
实例。新的CompletableFuture
返回的结果作为组合任务的结果。
@Test public void testCompose() throws Exception { System.out.println(getNowTime() + Thread.currentThread() + " main thread start!"); CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }); //thenCompose方法会在某个任务执行完成后,将该任务的执行结果作为方法入参然传入指定的方法 //该方法执行完成后会返回一个新的CompletableFuture实例 CompletableFuture<Integer> future3 = future1.thenCompose((res1) -> { System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); System.out.println("res1=" + res1); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); return CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task3 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getNowTime() + Thread.currentThread() + " task3 end"); return res1 + 88;//"组合"了上一个任务的结果 }); }); //主线程在没有调用CompletableFuture.get()之前都不会阻塞 System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); System.out.println("task3 result:" + future3.get()); }
applyToEither/acceptEither/runAfterEither
这3个方法都是将两个CompletableFuture组合起来,只要其中一个执行完成就会执行回调方法。
applyToEither
会将执行完成的任务的结果作为方法入参,并有返回值;
acceptEither
同样将执行完成的任务的结果作为方法入参,但没有返回值;
runAfterEither
既没有方法入参,也没有返回值。
注意两个任务中只要有一个执行异常,则在调用CompletableFuture.get()
时将该异常信息抛出。
@Test public void testEither() throws Exception { System.out.println(getNowTime() + Thread.currentThread() + " main thread start!"); CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }); CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); return 88; }); //thenCombine将两个任务的执行结果作为方法入参传递到指定方法中,且该方法有返回值 //task3要等到其组合的task1和task2任何一个执行完成后就会执行 CompletableFuture<Integer> future3 = future1.applyToEither(future2, (firstRes) -> { System.out.println(getNowTime() + Thread.currentThread() + " task3 start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task3 end"); return firstRes+100; }); //thenAcceptBoth也是将两个任务的执行结果作为方法入参,但是无返回值 //task4要等到其组合的task1和task2任何一个执行完成后就会执行 CompletableFuture<Void> future4 = future1.acceptEither(future2, (firstRes) -> { System.out.println(getNowTime() + Thread.currentThread() + " task4 start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("firstRes=" + firstRes); System.out.println(getNowTime() + Thread.currentThread() + " task4 end"); }); //runAfterBoth既没有入参,也没有返回值 //task5要等到其组合的task1和task2任何一个执行完成后就会执行 CompletableFuture<Void> future5 = future2.runAfterEither(future1, () -> { System.out.println(getNowTime() + Thread.currentThread() + " task5 start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getNowTime() + Thread.currentThread() + " task5 end"); }); //主线程在没有调用CompletableFuture.get()之前都不会阻塞 System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task1!"); System.out.println("task1 result:" + future1.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task2!"); System.out.println("task2 result:" + future2.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task3!"); System.out.println("task3 result:" + future3.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task4!"); System.out.println("task4 result:" + future4.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread go on,wait task5!"); System.out.println("task5 result:" + future5.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
allOf
allOf是多个组合的任务都执行完成后才会返回,只要有一个任务执行异常,则返回的CompletableFuture执行get()
方法时会抛出异常。如果都是正常执行,则get()
返回null
。
@Test public void testAll() throws Exception { System.out.println(getNowTime() + Thread.currentThread() + " main thread start!"); CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }); CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); return 88; }); CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task3 start"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task3 end"); return "OK"; }); //allOf是多个任务都执行完成后才会继续执行,只要有一个任务执行异常,则调用返回的CompletableFuture的get方法时会抛出异常 //如果都是正常执行,则get()返回null(这一点和anyOf不同,anyOf有返回值) //本例中只有task1、task2、task3全部完成,task4才会执行 CompletableFuture<Void> future4 = CompletableFuture.allOf(future1, future2, future3).whenComplete((r, e) -> { System.out.println(getNowTime() + Thread.currentThread() + " task4 start"); System.out.println("res:" + r); System.out.println("exception:" + e); System.out.println(getNowTime() + Thread.currentThread() + " task4 end"); }); //allOf().join()会阻塞程序,直到所有的任务执行完成,实际开发中经常使用这种方式等待多个任务执行完成再做下一步处理 //CompletableFuture.allOf(future1, future2, future3).join(); //主线程在没有调用CompletableFuture.get()之前都不会阻塞(在没有调allOf().join()时) System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); System.out.println("future4.get()=" + future4.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
anyOf
anyOf是多个组合的CompletableFuture中只要有一个执行完成就会返回,调用get()
方法返回的是已经执行完成的任务的返回结果。如果该任务执行异常,则调用get()
方法抛出异常。
@Test public void testAny() throws Exception { System.out.println(getNowTime() + Thread.currentThread() + " main thread start!"); CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task1 start"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task1 end"); return 100; }); CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task2 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (false) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task2 end"); return 88; }); CompletableFuture<String> future3 = CompletableFuture.supplyAsync(() -> { System.out.println(getNowTime() + Thread.currentThread() + " task3 start"); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } //异常会在调用CompletableFuture.get()时重新抛出 if (true) { throw new RuntimeException("some error"); } System.out.println(getNowTime() + Thread.currentThread() + " task3 end"); return "ok"; }); //anyOf是只要有一个任务执行完成,无论这个任务是正常执行或者执行异常,都会返回。 //本例中只要task1、task2、task3中的一个执行完成(或者执行异常)就会执行继续执行task4,task4.get的结果就是已执行完成的任务的结果 CompletableFuture<Object> future = CompletableFuture.anyOf(future1, future2, future3).whenComplete((r, e) -> { System.out.println(getNowTime() + Thread.currentThread() + " task4 start"); System.out.println("res:" + r); System.out.println("exception:" + e); System.out.println(getNowTime() + Thread.currentThread() + " task4 end"); }); //主线程在没有调用CompletableFuture.get()之前都不会阻塞 System.out.println(getNowTime() + Thread.currentThread() + "main thread go on!"); //anyOf返回的结果是先执行完成的那个任务的结果 CompletableFuture<Object> future5 = CompletableFuture.anyOf(future1, future2, future3); System.out.println("future5.get()=" + future5.get()); System.out.println("future.get()=" + future.get()); System.out.println(getNowTime() + Thread.currentThread() + "main thread over!"); }
Guava Cache教程
什么是缓存?
想必大家第一次听到“缓存”这个概念,还是在大学的计算机专业课上。学过操作系统原理、计算机组成原理的同学都知道,在计算机系统中,存储层级可按其作用分为高速缓冲存储器(Cache)、主存储器、辅助存储器三级。当然了,这里的“高速缓存”并非本文要讨论的缓存。今天我们要讨论的缓存是软件层面的。而高速缓存是位于CPU内部的物理器件,是主存与CPU之间的一级存储器,通常由静态存储芯片(SRAM)组成,容量很小但速度比主存快得多,接近于CPU的速度。其主要作用就是缓和CPU和内存之间速度不匹配的问题,使得整机处理速度得以提升。
尽管不同于硬件层面的高速缓存,但是缓存的思想本质上是一致的,都是将数据放在离使用者最近的位置以及访问速度较快的存储介质上以加快整个系统的处理速度。软件缓存可以认为是为了缓和客户端巨大的并发量和服务端数据库(通常是关系型数据库)处理速度不匹配的问题。缓存本质上是在内存中维护的一个hash数据结构(哈希表),通常是以<key,value>的形式存储。由于hash table查询时间复杂度为O(1),因此性能很高。能够有效地加速应用的读写速度,降低后端负载。
那么自行实现一个缓存需要考虑哪些基本问题呢?
1)数据结构
首先要考虑的是数据该如何存储,要选择合适的数据结构。在Java编程中,最简单的就是直接用Map集合来存储数据;复杂一点,像redis提供了多种数据结构:哈希,列表,集合,有序集合等,底层使用了双端链表,压缩列表,跳跃表等数据结构;
2)更新/清除策略
缓存中的数据都是有生命周期的,要在指定时间后被删除或更新,这样才能保证缓存空间在一个可控的范围。常用的更新/清除策略有LRU(Least Recently Used最近最少使用)、FIFO( First Input First Output先进先出)、LFU(Least Frequently Used最近最不常用)、SOFT(软引用)、WEAK(弱引用)等策略。
3)线程安全
redis是单线程处理模式,就不存在线程安全问题;而本地缓存往往是可以多个线程同时访问的,所以线程安全不容忽视;线程安全问题是不应该抛给使用者去保证的,因此需要缓存自身支持。
常用的缓存技术有哪些呢?
1)分布式缓存
Memcached
Memcached 是一个开源的、高性能的、分布式的、基于内存的对象缓存系统。它能够用来存储各种格式的数据,包括字符串、图像、视频、文件等。Memcached 把数据全部存在内存之中,断电后会丢失,因此数据不能超过内存大小。且支持的数据结构较为单一,一般用于简单的key-value形式的存储。
Redis
Redis是一个开源的基于内存的数据结构存储组件,可用作数据库(nosql)、缓存和消息队列。它支持诸如字符串、散列、列表、集合、带范围查询的有序集合、位图、hyperloglogs、支持半径查询和流的地理空间索引等多种数据结构。Redis具有内置的复制、Lua脚本、LRU清除、事务和不同级别的磁盘持久化特性,并通过Redis 哨兵机制和基于Redis集群的自动分区提供高可用性。
和Memcached 相比,Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等多种数据结构的存储。Redis还支持定期把数据持久化到磁盘。
2)本地缓存
本地缓存,顾名思义就是在应用本身维护一个缓存结构,比如Java中的Map集合就比较适合做<key,value>缓存。本地应用缓存最大的优点是应用本身和Cache在同一个进程内部,请求缓存非常快速,没有额外的网络I/O开销。本地缓存适合于单应用中不需要集群、各节点无需互相通信的场景。因此,其缺点是缓存跟应用程序耦合,分布式场景下多个独立部署的应用程序无法直接共享缓存,各节点都需要维护自己的单独缓存,既是对物理内存的一种浪费,也会导致数据的不一致性。Guava cache就是一种本地缓存。
话不多说,进入正题,本文主要介绍Google的缓存组件Guava Cache的实战技巧!
1.引入依赖
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.1-jre</version> </dependency>
2.Demo1:简单使用,掌握如何创建使用Cache
import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.concurrent.Callable; public class CacheService1 { public static void main(String[] args) throws Exception { Cache<Object, Object> cache = CacheBuilder.newBuilder().build(); // 写入/覆盖一个缓存 cache.put("k1", "v1"); // 获取一个缓存,如果该缓存不存在则返回一个null值 Object value1 = cache.getIfPresent("k1"); System.out.println("value1:" + value1); // 获取缓存,当缓存不存在时,则通Callable进行加载并返回,该操作是原子的 Object getValue1 = cache.get("k1", new Callable<Object>() { @Override public Object call() throws Exception { //缓存加载逻辑 return null; } }); System.out.println("getValue1:" + getValue1); Object getValue2 = cache.get("k2", new Callable<Object>() { /** * 加载缓存的逻辑 * @return * @throws Exception */ @Override public Object call() throws Exception { return "v2"; } }); System.out.println("getValue2:" + getValue2); } }
控制台输出:
value1:v1
getValue1:v1
getValue2:v2
上述程序演示了Guava Cache的读和写。Guava的缓存有许多配置选项,为了简化缓的创建,使用了Builder设计模式;Builder使用的是链式编程的思想,也就是每次调用方法后返回的是对象本身,这样可以简化配置过程。
获取缓存值时可以指定一个Callable实例动态执行缓存加载逻辑;也可以在创建Cache实例时直接使用LoadingCache。顾名思义,它能够通过CacheLoader自发的加载缓存。
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; public class CacheService2 { public static void main(String[] args) throws Exception { LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder() .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // 缓存加载逻辑 return "value2"; } }); loadingCache.put("k1", "value1"); String v1 = loadingCache.get("k1"); System.out.println(v1); // 以不安全的方式获取缓存,当缓存不存在时,会通过CacheLoader自动加载 String v2 = loadingCache.getUnchecked("k2"); System.out.println(v2); // 获取缓存,当缓存不存在时,会通过CacheLoader自动加载 String v3 = loadingCache.get("k3"); System.out.println(v3); } }
控制台输出:
value1
value2
value2
3.Demo2:理解Cache的过期处理机制
import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit; public class CacheService3 { /** * 通过builder模式创建一个Cache实例 */ static Cache<Integer, String> cache = CacheBuilder.newBuilder() //设置缓存在写入5秒钟后失效 .expireAfterWrite(5, TimeUnit.SECONDS) //设置缓存的最大容量(基于容量的清除) .maximumSize(1000) //开启缓存统计 .recordStats() .build(); public static void main(String[] args) throws Exception { //单起一个线程监视缓存状态 new Thread() { public void run() { while (true) { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); System.out.println(sdf.format(new Date()) + " cache size: " + cache.size()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }.start(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); //写入缓存 cache.put(1, "value1"); //读取缓存 System.out.println("write key:1 ,value:" + cache.getIfPresent(1)); Thread.sleep(10000); // when write ,key:1 clear cache.put(2, "value2"); System.out.println("write key:2 ,value:" + cache.getIfPresent(2)); Thread.sleep(10000); // when read other key ,key:2 do not clear System.out.println(sdf.format(new Date()) + " after write, key:1 ,value:" + cache.getIfPresent(1)); Thread.sleep(2000); // when read same key ,key:2 clear System.out.println(sdf.format(new Date()) + " final, key:2 ,value:" + cache.getIfPresent(2)); Thread.sleep(2000); cache.put(1, "value1"); cache.put(2, "value2"); Thread.sleep(3000); System.out.println(sdf.format(new Date()) + " write key:1 ,value:" + cache.getIfPresent(1)); System.out.println(sdf.format(new Date()) + " write key:2 ,value:" + cache.getIfPresent(2)); Thread.sleep(3000); System.out.println(sdf.format(new Date()) + " final key:1 ,value:" + cache.getIfPresent(1)); System.out.println(sdf.format(new Date()) + " final key:2 ,value:" + cache.getIfPresent(2)); Thread.sleep(3000); } }
控制台输出:
22:07:17 cache size: 0
write key:1 ,value:value1
22:07:18 cache size: 1
22:07:19 cache size: 1
22:07:20 cache size: 1
22:07:21 cache size: 1
22:07:22 cache size: 1
22:07:23 cache size: 1
22:07:24 cache size: 1
22:07:25 cache size: 1
22:07:26 cache size: 1
22:07:27 cache size: 1
write key:2 ,value:value2
22:07:28 cache size: 1
22:07:29 cache size: 1
22:07:30 cache size: 1
22:07:31 cache size: 1
22:07:32 cache size: 1
22:07:33 cache size: 1
22:07:34 cache size: 1
22:07:35 cache size: 1
22:07:36 cache size: 1
22:07:37 after write, key:1 ,value:null
22:07:37 cache size: 1
22:07:38 cache size: 1
22:07:39 final, key:2 ,value:null
22:07:39 cache size: 0
22:07:40 cache size: 0
22:07:41 cache size: 2
22:07:42 cache size: 2
22:07:43 cache size: 2
22:07:44 write key:1 ,value:value1
22:07:44 write key:2 ,value:value2
22:07:44 cache size: 2
22:07:45 cache size: 2
22:07:46 cache size: 2
22:07:47 final key:1 ,value:null
22:07:47 final key:2 ,value:null
22:07:47 cache size: 0
22:07:48 cache size: 0
运行上述程序,可以得出如下结论:
(1)缓存项<1,"value1">的过期时间是5秒,但经过5秒后并没有被清除,因为还是size=1
(2)发生写操作cache.put(2, "value2")后,缓存项<1,"value1">被清除,因为size=1,而不是size=2
(3)发生读操作cache.getIfPresent(1)后,缓存项<2,"value2">没有被清除,因为还是size=1,即读操作确实不一定触发清除
(4)发生读操作cache.getIfPresent(2)后,缓存项<2,"value2">被清除,因为读的key就是2
上述机制在Guava Cache中被称为“延迟删除”,即删除总是发生得比较“晚”,并不是真正意义上的定时过期。需要依靠用户请求线程下一次读写缓存才能触发清除/更新。这也是Guava Cache的独到之处。但是这种实现方式也会存在问题:缓存会可能会存活比较长的时间,一直占用着物理内存。如果使用了复杂的清除策略如基于容量的清除,还可能会占用着线程而导致响应时间变长。但优点也是显而易见的,没有启动额外的线程,不管是实现,还是使用都比较简单(轻量)。
如果我们需要尽可能地降低延迟,可以运行自己的维护线程,以固定的时间间隔调用Cache.cleanUp()。借助ScheduledExecutorService就可以实现这样的定时调度。不过这种方式依然没办法百分百的确定一定是自己的维护线程“命中”了维护的工作。
如果使用了LoadingCache并且重写了load 方法,则缓存过期时调用get方法会自动执行load逻辑。调用getIfPresent方法获取已经过期的缓存不会执行load方法,只会执行删除策略。代码如下
static LoadingCache<Integer, String> cache = CacheBuilder.newBuilder() //设置缓存在写入5秒钟后失效 .expireAfterWrite(3, TimeUnit.SECONDS) //设置缓存的最大容量(基于容量的清除) .maximumSize(1000) //开启缓存统计 .recordStats() .build(new CacheLoader<Integer, String>() { @Override public String load(Integer key) throws Exception { return "new value"; } });
4. Demo3: 多线程并发获取key的场景
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.util.concurrent.ExecutionException; public class CacheService4 { public static void main(String[] args) throws Exception { LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder() .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // 缓存加载逻辑 System.out.println("执行缓存加载逻辑,key:" + key); return "value1"; } }); // 获取缓存,当缓存不存在时,会通过CacheLoader自动加载 String v1 = loadingCache.get("k"); System.out.println("value:" + v1); //模拟多线程同时获取相同的key for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { String vv = null; try { vv = loadingCache.get("kk"); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("value:" + vv); } }).start(); } Thread.sleep(1000); System.out.println("---------------------------------------"); //模拟多线程同时获取不同的key for (int j = 0; j < 10; j++) { int i = j; new Thread(new Runnable() { @Override public void run() { String vv = null; try { vv = loadingCache.get("k" + i); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("value:" + vv); } }).start(); } } }
控制台输出:
执行缓存加载逻辑,key:k
value:value1
执行缓存加载逻辑,key:kk
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1
value:value1
---------------------------------------
执行缓存加载逻辑,key:k0
执行缓存加载逻辑,key:k1
value:value1
value:value1
执行缓存加载逻辑,key:k2
value:value1
执行缓存加载逻辑,key:k3
value:value1
执行缓存加载逻辑,key:k4
value:value1
执行缓存加载逻辑,key:k6
value:value1
执行缓存加载逻辑,key:k7
value:value1
执行缓存加载逻辑,key:k8
value:value1
执行缓存加载逻辑,key:k5
value:value1
执行缓存加载逻辑,key:k9
value:value1
由此可见,当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其他线程则等待,直到缓存值被生成。这样也就避免了缓存击穿的危险。
5.Demo4:定时刷新和定时过期的比较
上述使用方法,虽然规避了缓存击穿的情况,但是每当某个缓存值过期时,会导致大量的请求线程被阻塞。而Guava则提供了另一种缓存策略,缓存值定时刷新:只有一个更新线程调用load方法更新该缓存,其他请求线程先返回该缓存的旧值。这样对于某个key的缓存来说,只会有一个线程被阻塞,用来生成缓存值,而其他的线程都返回旧的缓存值,不会被阻塞。
Demo4-1:阻塞的情况(定时过期)
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; public class CacheService5 { public static void main(String[] args) throws Exception { LoadingCache<String, String> loadingCache = CacheBuilder.newBuilder() .expireAfterWrite(2, TimeUnit.SECONDS) //.refreshAfterWrite(2, TimeUnit.SECONDS) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // 缓存加载逻辑 System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " 执行缓存加载逻辑,key:" + key); Thread.sleep(2000); return "new value"; } }); loadingCache.put("kk", "old value"); System.out.println("kk:" + loadingCache.get("kk")); Thread.sleep(2000); //模拟多线程同时获取相同的key for (int i = 0; i < 10; i++) { new Thread(new Runnable() { @Override public void run() { String vv = null; try { vv = loadingCache.get("kk"); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + "-value:" + vv); } }).start(); } }
控制台输出:
kk:old value
22:46:00Thread-1 执行缓存加载逻辑,key:kk
22:46:02Thread-5-value:new value
22:46:02Thread-10-value:new value
22:46:02Thread-3-value:new value
22:46:02Thread-2-value:new value
22:46:02Thread-8-value:new value
22:46:02Thread-7-value:new value
22:46:02Thread-6-value:new value
22:46:02Thread-1-value:new value
22:46:02Thread-4-value:new value
22:46:02Thread-9-value:new value
由控制台输出可见所有线程阻塞直到取到新值。
Demo4-2:非阻塞的情况(定时刷新)
放开refresh这行注释,由控制台输出可见只有Thread-1获取新值,其它线程立即返回旧值
.refreshAfterWrite(2, TimeUnit.SECONDS) //.expireAfterWrite(2, TimeUnit.SECONDS)
控制台输出:
kk:old value
22:49:47Thread-3-value:old value
22:49:47Thread-5-value:old value
22:49:47Thread-10-value:old value
22:49:47Thread-1 执行缓存加载逻辑,key:kk
22:49:47Thread-6-value:old value
22:49:47Thread-7-value:old value
22:49:47Thread-8-value:old value
22:49:47Thread-9-value:old value
22:49:47Thread-2-value:old value
22:49:47Thread-4-value:old value
22:49:49Thread-1-value:new value
另外需要注意的是,此处的定时并非真正意义上的定时。Guava cache的刷新需要依靠用户请求线程,只有该线程调用load方法时才会校验是否过期,所以如果一直没有用户尝试获取该缓存值,则该缓存也并不会刷新。
5. Demo5:使用异步刷新
上述方法解决的是同一个key缓存过期时多个加载相同key的线程阻塞的问题,即保证只有一个用户线程被阻塞。如果是多个不同的key呢?当缓存的key很多时,高并发环境下大量线程同时获取不同key对应的缓存,此时依然会造成大量线程阻塞。
解决方案:将刷新缓存值的任务交给后台线程,所有的用户请求线程均返回旧的缓存值(刷新完成后再访问才会是新值),这样就不会有用户线程被阻塞了。
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class CacheService6 { public static void main(String[] args) { ListeningExecutorService backgroundRefreshPools = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); AtomicInteger count = new AtomicInteger(0); LoadingCache<String, Object> cache = CacheBuilder.newBuilder() .refreshAfterWrite(2, TimeUnit.SECONDS) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " 正常加载逻辑,key:" + key); int i = count.addAndGet(1); Thread.sleep(1000);//模拟耗时 return "value" + i; } @Override public ListenableFuture<Object> reload(String key, Object oldValue) throws Exception { return backgroundRefreshPools.submit(new Callable<Object>() { @Override public Object call() throws Exception { int i = count.addAndGet(1); System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " 异步刷新逻辑,key:" + key + ",value:" + i); Thread.sleep(1000);//模拟耗时 return "value" + i; } }); } }); try { System.out.println("--------------------------0轮--------------------------------------"); for (int i = 1; i <= 10; i++) { cache.put("k" + i, "v" + i); } for (int i = 1; i <= 10; i++) { System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " k" + i + ":" + cache.get("k" + i)); } Thread.sleep(2000); System.out.println("--------------------------1轮--先返回第0轮的旧值----------------------------"); for (int i = 1; i <= 10; i++) { System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " k" + i + ":" + cache.get("k" + i)); } Thread.sleep(3000); System.out.println("--------------------------2轮--先返回第1轮的旧值----------------------------"); for (int i = 1; i <= 10; i++) { System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " k" + i + ":" + cache.get("k" + i)); } Thread.sleep(3000); System.out.println("--------------------------3轮--先返回第2轮的旧值----------------------------"); for (int i = 1; i <= 10; i++) { System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + Thread.currentThread().getName() + " k" + i + ":" + cache.get("k" + i)); } } catch (Exception e) { e.printStackTrace(); } } }
控制台输出如下,由此可见缓存过期后再访问都是立即返回旧值(缓存现有值),同时触发异步刷新,下一次用户现线程访问时才能获取到更新值。从下列输出还可以发现,由于线程池大小设置很小、刷新过程有时间开销,来不及刷新就被再次访问时还是输出旧值(缓存现有值)。
------------0轮------------ 14:39:32main k1:v1 14:39:32main k2:v2 14:39:32main k3:v3 14:39:32main k4:v4 14:39:32main k5:v5 14:39:32main k6:v6 14:39:32main k7:v7 14:39:32main k8:v8 14:39:32main k9:v9 14:39:32main k10:v10 -------------1轮--先返回第0轮的旧值------------ 14:39:34main k1:v1 14:39:34main k2:v2 14:39:34main k3:v3 14:39:34main k4:v4 14:39:34pool-1-thread-1 异步刷新逻辑,key:k1,value:1 14:39:34pool-1-thread-2 异步刷新逻辑,key:k2,value:2 14:39:34main k5:v5 14:39:34pool-1-thread-3 异步刷新逻辑,key:k3,value:3 14:39:34main k6:v6 14:39:34pool-1-thread-4 异步刷新逻辑,key:k4,value:4 14:39:34main k7:v7 14:39:34main k8:v8 14:39:34pool-1-thread-5 异步刷新逻辑,key:k5,value:5 14:39:34main k9:v9 14:39:34pool-1-thread-6 异步刷新逻辑,key:k6,value:6 14:39:34pool-1-thread-7 异步刷新逻辑,key:k7,value:7 14:39:34main k10:v10 14:39:34pool-1-thread-8 异步刷新逻辑,key:k8,value:8 14:39:34pool-1-thread-9 异步刷新逻辑,key:k9,value:9 14:39:34pool-1-thread-10 异步刷新逻辑,key:k10,value:10 ------------2轮--先返回第1轮的旧值------------- 14:39:37main k1:value1 14:39:37main k2:value2 14:39:37pool-1-thread-2 异步刷新逻辑,key:k2,value:11 14:39:37main k3:value3 14:39:37pool-1-thread-1 异步刷新逻辑,key:k3,value:12 14:39:37main k4:value4 14:39:37pool-1-thread-5 异步刷新逻辑,key:k4,value:13 14:39:37main k5:value5 14:39:37main k6:value6 14:39:37main k7:value7 14:39:37pool-1-thread-3 异步刷新逻辑,key:k7,value:14 14:39:37main k8:value8 14:39:37main k9:value9 14:39:37pool-1-thread-6 异步刷新逻辑,key:k9,value:15 14:39:37main k10:value10 -----------3轮--先返回第2轮的旧值-------------- 14:39:40main k1:value1 14:39:40pool-1-thread-8 异步刷新逻辑,key:k1,value:16 14:39:40main k2:value11 14:39:40pool-1-thread-4 异步刷新逻辑,key:k2,value:17 14:39:40main k3:value12 14:39:40pool-1-thread-7 异步刷新逻辑,key:k3,value:18 14:39:40main k4:value13 14:39:40pool-1-thread-9 异步刷新逻辑,key:k4,value:19 14:39:40main k5:value5 14:39:40pool-1-thread-10 异步刷新逻辑,key:k5,value:20 14:39:40main k6:value6 14:39:40main k7:value14 14:39:40pool-1-thread-2 异步刷新逻辑,key:k6,value:21 14:39:40main k8:value8 14:39:40pool-1-thread-5 异步刷新逻辑,key:k8,value:22 14:39:40main k9:value15 14:39:40pool-1-thread-1 异步刷新逻辑,key:k9,value:23 14:39:40main k10:value10 14:39:40pool-1-thread-6 异步刷新逻辑,key:k10,value:24
如果删除demo中的reload方法,则会缓存过期后访问会调用load方法,每个线程同步阻塞调用,输出如下:
--------------------------0轮---------------------------
17:45:01main k1:v1
17:45:01main k2:v2
17:45:01main k3:v3
17:45:01main k4:v4
17:45:01main k5:v5
17:45:01main k6:v6
17:45:01main k7:v7
17:45:01main k8:v8
17:45:01main k9:v9
17:45:01main k10:v10
-------------1轮--先返回第0轮的旧值------------
17:45:03main 正常加载逻辑,key:k1
17:45:03main k1:value1
17:45:04main 正常加载逻辑,key:k2
17:45:04main k2:value2
17:45:05main 正常加载逻辑,key:k3
17:45:05main k3:value3
17:45:06main 正常加载逻辑,key:k4
17:45:06main k4:value4
17:45:07main 正常加载逻辑,key:k5
17:45:07main k5:value5
17:45:08main 正常加载逻辑,key:k6
17:45:08main k6:value6
17:45:09main 正常加载逻辑,key:k7
17:45:09main k7:value7
17:45:10main 正常加载逻辑,key:k8
17:45:10main k8:value8
17:45:11main 正常加载逻辑,key:k9
17:45:11main k9:value9
17:45:12main 正常加载逻辑,key:k10
17:45:12main k10:value10
------------2轮--先返回第1轮的旧值-------------
17:45:16main 正常加载逻辑,key:k1
17:45:16main k1:value11
17:45:17main 正常加载逻辑,key:k2
17:45:17main k2:value12
17:45:18main 正常加载逻辑,key:k3
17:45:18main k3:value13
17:45:19main 正常加载逻辑,key:k4
17:45:19main k4:value14
17:45:20main 正常加载逻辑,key:k5
17:45:20main k5:value15
17:45:21main 正常加载逻辑,key:k6
17:45:21main k6:value16
17:45:22main 正常加载逻辑,key:k7
17:45:22main k7:value17
17:45:23main 正常加载逻辑,key:k8
17:45:23main k8:value18
17:45:24main 正常加载逻辑,key:k9
17:45:24main k9:value19
17:45:25main 正常加载逻辑,key:k10
17:45:25main k10:value20
-----------3轮--先返回第2轮的旧值-------------
17:45:30main 正常加载逻辑,key:k1
17:45:30main k1:value21
17:45:31main 正常加载逻辑,key:k2
17:45:31main k2:value22
17:45:32main 正常加载逻辑,key:k3
17:45:32main k3:value23
17:45:33main 正常加载逻辑,key:k4
17:45:33main k4:value24
17:45:34main 正常加载逻辑,key:k5
17:45:34main k5:value25
17:45:35main 正常加载逻辑,key:k6
17:45:35main k6:value26
17:45:36main 正常加载逻辑,key:k7
17:45:36main k7:value27
17:45:37main 正常加载逻辑,key:k8
17:45:37main k8:value28
17:45:38main 正常加载逻辑,key:k9
17:45:38main k9:value29
17:45:39main 正常加载逻辑,key:k10
17:45:39main k10:value30
因此建议使用Guava Cache时都要实现异步化的reload方法。
7.Demo6: 一个较为完整的案例
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public abstract class BaseGuavaCache<K, V> { private Logger logger = LoggerFactory.getLogger(getClass()); // 缓存自动刷新周期 protected int refreshDuration = 10; // 缓存刷新周期时间格式 protected TimeUnit refreshTimeunit = TimeUnit.MINUTES; // 缓存过期时间(可选择) protected int expireDuration = -1; // 缓存刷新周期时间格式 protected TimeUnit expireTimeunit = TimeUnit.HOURS; // 缓存最大容量 protected int maxSize = 4; // 数据刷新线程池 protected static ListeningExecutorService refreshPool = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20)); private LoadingCache<K, V> cache = null; /** * 用于初始化缓存值(例如某些场景下系统启动自动加载缓存) */ public abstract void loadValueWhenStarted(); /** * 缓存计算、加载逻辑 * * @param key * @return * @throws Exception */ protected abstract V getValueWhenExpired(K key) throws Exception; /** * 获取缓存 * * @param key * @return * @throws Exception */ public V getValue(K key) throws Exception { try { return getCache().get(key); } catch (Exception e) { logger.error("从内存缓存中获取内容时发生异常,key: " + key, e); throw e; } } public V getValueOrDefault(K key, V defaultValue) { try { return getCache().get(key); } catch (Exception e) { logger.error("从内存缓存中获取内容时发生异常,key: " + key, e); return defaultValue; } } /** * 设置基本属性 */ public BaseGuavaCache<K, V> setRefreshDuration(int refreshDuration) { this.refreshDuration = refreshDuration; return this; } public BaseGuavaCache<K, V> setRefreshTimeUnit(TimeUnit refreshTimeunit) { this.refreshTimeunit = refreshTimeunit; return this; } public BaseGuavaCache<K, V> setExpireDuration(int expireDuration) { this.expireDuration = expireDuration; return this; } public BaseGuavaCache<K, V> setExpireTimeUnit(TimeUnit expireTimeunit) { this.expireTimeunit = expireTimeunit; return this; } public BaseGuavaCache<K, V> setMaxSize(int maxSize) { this.maxSize = maxSize; return this; } public void clearAll() { this.getCache().invalidateAll(); } /** * 获取cache单例 * * @return */ private LoadingCache<K, V> getCache() { if (cache == null) { synchronized (this) { if (cache == null) { CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder().maximumSize(maxSize); if (refreshDuration > 0) { cacheBuilder = cacheBuilder.refreshAfterWrite(refreshDuration, refreshTimeunit); } if (expireDuration > 0) { cacheBuilder = cacheBuilder.expireAfterWrite(expireDuration, expireTimeunit); } cache = cacheBuilder.build(new CacheLoader<K, V>() { @Override public V load(K key) throws Exception { return getValueWhenExpired(key); } @Override public ListenableFuture<V> reload(final K key, V oldValue) throws Exception { return refreshPool.submit(new Callable<V>() { public V call() throws Exception { return getValueWhenExpired(key); } }); } }); } } } return cache; } }
实际业务开发时继承、实现上述抽象类,自定义业务缓存完美搞定!建议设置的刷新时间refreshDuration远小于过期时间expireDuration 。
总结一下,GuavaCache是一款面向本地缓存的,轻量级的Cache,适合缓存少量数据。如果你想缓存上千万数据,可以为每个key设置不同的存活时间,并且高性能,那并不适合使用Guava Cache。Guava Cache中的所有维护操作,包括清除过期缓存、写入缓存等,都是通过调用线程来驱动的,和Redis的单线程似乎有异曲同工之妙!需要起本地缓存提高响应速度,减少数据库调用频次的场景下推荐使用!