1 Introduction
At the beginning of work, the concept of asynchronous programming may stay in the concept of threads and thread pools. Create a new thread to execute, only execute with Runnable
, if you want a return value, use Callable
to return Future
. To be more experienced, use a thread pool to manage threads. But these are still in the basic stage, and various complex application scenarios will be encountered during actual development. Although the concurrent package also provides some collaboration classes like Semaphore
, CountDownLatch
, CyclicBarrier
, but it is still not enough, here is an introduction to jdk8 launch CompletableFuture
.
Let’s first list a few problems encountered in actual development:
- There is an asynchronous call to method A in the code, and another method B needs to process the result returned by method A after the invocation of method A is completed. If you wait for the completion of method A through
future.get()
, you will undoubtedly return to synchronous blocking. - There are multiple asynchronous call methods A, B, C, and D in the code, but the final result cannot be returned until all asynchronous methods are executed. This scenario is also relatively common. In a microservice project, an interface needs to obtain data from different business services at the same time.
These problems are not a problem after learning CompletableFuture
. Its improvement for traditional asynchronous programming is mainly reflected in the support of asynchronous callbacks. The improvement of CompletableFuture to traditional asynchronous programming, I think it is a bit like AIO (asynchronous non-blocking) to NIO (synchronous non-blocking) .
The following is an introduction to the usage of CompletableFuture
. Its methods are many and complicated, and many functions overlap. After trying each method, I made a classification according to my own understanding, and each classification is explained in the following single chapter.
2. Basics
The CompletableFuture class implements the Future and CompletionStage interfaces, so the methods provided by the original Future can still be used, but the new features are all in the CompletionStage.
Create CompletableFuture
CompletableFuture provides several static methods that use tasks to instantiate a CompletableFuture instance.
CompletableFuture<Void> CompletableFuture.runAsync(Runnable runnable);
CompletableFuture<Void> CompletableFuture.runAsync(Runnable runnable, Executor executor);
CompletableFuture<U> CompletableFuture.supplyAsync(Supplier<U> supplier);
CompletableFuture<U> CompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor)
Here are a few differences:
runAsync
similar toRunnable
. It focuses on executing tasks and has no return value, so it returns CompletableFuture<Void>. AndsupplyAsync
similar toCallable
and has the return value CompletableFuture<U>.- The overloaded method with
executor
of these two methods indicates that the task is executed in the specified thread pool. If it is not specified, the task is usually executed in theForkJoinPool.commonPool()
thread pool. The other CompletableFuture methods introduced later basically have overloaded methods with executors.
Future method
Because the Future interface is implemented, the methods provided by the original Future can still be used.
public T get()
public T get(long timeout, TimeUnit unit)
public T getNow(T valueIfAbsent)
public T join()
getNow
is a bit special, if the result has been calculated, it returns the result or throws an exception, otherwise it returns the given valueIfAbsent value.join
returns the result of the calculation or throws an unchecked exception (CompletionException), which is slightly different from how get handles the thrown exception.
shows done
boolean complete(T value)
: Sets the value returned by get() and related methods to the given value if not already done. If done, return the get() value normally.boolean completeExceptionally(Throwable ex)
: Causes calls to get() and related methods to throw the given exception if it has not completed. If done, return the get() value normally.
There is an example:
public static CompletableFuture<Integer> divideNumber(int a, int b) {
return CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return a / b;
});
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
log.debug("begin");
CompletableFuture<Integer> intFuture = divideNumber(10, 2);
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
intFuture.complete(0);
}).start();
CompletableFuture<Integer> exceptionFuture = divideNumber(10, 2);
new Thread(()->{
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
exceptionFuture.completeExceptionally(new RuntimeException("exceptionFuture 没有执行完"));
}).start();
log.debug("(没执行完就返回0) result:{}", intFuture.get());
log.debug("(没执行完就报错) result:{}",exceptionFuture.get());
}
Return result:
18:06:56.179 [main] DEBUG pers.kerry.aservice.service.CFComplete - begin
18:06:59.245 [main] DEBUG pers.kerry.aservice.service.CFComplete - (没执行完就返回0) result:0
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException: exceptionFuture 没有执行完
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
at pers.kerry.aservice.service.CFComplete.main(CFComplete.java:50)
Caused by: java.lang.RuntimeException: exceptionFuture 没有执行完
at pers.kerry.aservice.service.CFComplete.lambda$main$2(CFComplete.java:46)
at java.lang.Thread.run(Thread.java:748)
3. Function method
Function method means that the input parameters of the following methods are the functional interface Function
or BiFunction
(Bi is an abbreviation for Bidirectional, which refers to a Function with two input parameters). Well, the function is similar to the map
and flatMap
methods in Stream, which return the CompletableFuture whose current internal data has been mapped and converted. So they all wait for the previous CompletableFuture to complete.
Let’s first introduce the functions one by one, and then do a comparison.
3.1. thenApply
The thenApply method is defined as:
public <U> CompletableFuture<U> thenApply(
Function<? super T,? extends U> fn)
Features are:
- Enter the parameter Function T -> U, if the former CompletableFuture does not report an error, after normal completion, the thenApply method will be executed.
- There is no Throwable in the input parameter Function, so there is no exception handling. If the former CompletableFuture reports an error, an exception will be thrown directly and the method will not be executed.
Sample code:
public static CompletableFuture<Integer> divideNumber(int a, int b) {
return CompletableFuture.supplyAsync(() -> a / b);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> intFuture = divideNumber(10, 2)
.thenApply(param -> param * 10);
CompletableFuture<String> stringFuture = divideNumber(10, 2)
.thenApply(param -> "这是字符串-" + param);
CompletableFuture<Integer> exceptionFuture = divideNumber(10, 0)
.thenApply(param -> param * 10);
log.debug("intFuture result:{}", intFuture.get());
log.debug("stringFuture result:{}", stringFuture.get());
log.debug("exceptionFuture result:{}", exceptionFuture.get());
}
The print log is:
22:39:23.478 [main] DEBUG pers.kerry.aservice.service.CFApply - intFuture result:50
22:39:23.485 [main] DEBUG pers.kerry.aservice.service.CFApply - stringFuture result:这是字符串-5
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
at pers.kerry.aservice.service.CFApply.main(CFApply.java:36)
Caused by: java.lang.ArithmeticException: / by zero
at pers.kerry.aservice.service.CFApply.lambda$divideNumber$0(CFApply.java:21)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
3.2. exceptionally
The exceptionally method is defined as:
public CompletableFuture<T> exceptionally(
Function<Throwable, ? extends T> fn)
Features are:
- Enter the parameter Function Throwable -> T. If the former CompletableFuture does not report an error, the exceptionally method will not be triggered.
- There is Throwable in the input parameter Function, so it can handle exceptions. If the former CompletableFuture reports an error, it will trigger the method and return a custom value to deal with the error.
Sample code:
public static CompletableFuture<Integer> divideNumber(int a, int b) {
return CompletableFuture.supplyAsync(() -> a / b);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> normalFuture = divideNumber(10, 2)
.exceptionally((throwable -> {
return 0;
}));
CompletableFuture<Integer> exceptionFuture = divideNumber(10, 0)
.exceptionally((throwable -> {
return 0;
}));
log.debug("normalFuture result:{}", normalFuture.get());
log.debug("exceptionFuture result:{}", exceptionFuture.get());
}
The print log is:
22:53:34.335 [main] DEBUG pers.kerry.aservice.service.CFExceptionally - normalFuture result:5
22:53:34.342 [main] DEBUG pers.kerry.aservice.service.CFExceptionally - exceptionFuture result:0
3.3. handle
The handle method is defined as:
public <U> CompletableFuture<U> handle(
BiFunction<? super T, Throwable, ? extends U> fn)
Features are:
- Enter BiFunction T, Throwable -> U, whether an error is reported or not, the handle method will be triggered after the execution is completed.
- There is Throwable in the input parameter Function, so it can handle exceptions. If the former CompletableFuture reports an error, it will trigger the method and return a custom value to deal with the error.
Sample code:
public static CompletableFuture<Integer> divideNumber(int a, int b) {
return CompletableFuture.supplyAsync(() -> a / b);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> intFuture = divideNumber(10, 2)
.handle((value, throwable) -> value * 10);
CompletableFuture<String> stringFuture = divideNumber(10, 2)
.handle((value, throwable) -> "这是字符串-" + value);
CompletableFuture<Integer> exceptionFuture = divideNumber(10, 0)
.handle((value, throwable) -> {
if (throwable != null) {
return 0;
}
return value * 10;
});
log.debug("intFuture result:{}", intFuture.get());
log.debug("stringFuture result:{}", stringFuture.get());
log.debug("exceptionFuture result:{}", exceptionFuture.get());
}
The print log is:
23:03:08.771 [main] DEBUG pers.kerry.aservice.service.CFHandle - intFuture result:50
23:03:08.782 [main] DEBUG pers.kerry.aservice.service.CFHandle - stringFuture result:这是字符串-5
23:03:08.782 [main] DEBUG pers.kerry.aservice.service.CFHandle - exceptionFuture result:0
3.4. thenCompose
The thenCompose method is defined as:
public <U> CompletableFuture<U> thenCompose(
Function<? super T, ? extends CompletionStage<U>> fn)
Features are:
- Enter the parameter Function T -> CompletionStage<U>, if the former CompletableFuture does not report an error, after normal completion, the thenCompose method will be executed.
- There is no Throwable in the input parameter Function, so there is no exception handling. If the former CompletableFuture reports an error, an exception will be thrown directly and the method will not be executed.
Sample code:
public static CompletableFuture<Integer> divideNumber(int a, int b) {
return CompletableFuture.supplyAsync(() -> a / b);
}
public static CompletableFuture<Integer> multiplyTen(int a) {
return CompletableFuture.supplyAsync(() -> a * 10);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> intFuture = divideNumber(10, 2)
.thenCompose(CFCompose::multiplyTen);
CompletableFuture<Integer> exceptionFuture = divideNumber(10, 0)
.thenCompose(CFCompose::multiplyTen);
log.debug("intFuture result:{}", intFuture.get());
log.debug("exceptionFuture result:{}", exceptionFuture.get());
}
The print log is:
23:47:18.967 [main] DEBUG pers.kerry.aservice.service.CFCompose - intFuture result:50
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
at pers.kerry.aservice.service.CFCompose.main(CFCompose.java:48)
Caused by: java.lang.ArithmeticException: / by zero
at pers.kerry.aservice.service.CFCompose.lambda$divideNumber$0(CFCompose.java:35)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
3.5. thenCombine
The thenCombine method is defined as:
public <U,V> CompletableFuture<V> thenCombine(
CompletionStage<? extends U> other,
BiFunction<? super T,? super U,? extends V> fn)
Features are:
- Enter the parameter Function T -> U. If the former CompletableFuture and CompletionStage<? extends U> other do not report an error, and both are completed normally, the thenCombine method will be executed.
- There is no Throwable in the input parameter Function, so there is no exception handling. If the former CompletableFuture or CompletionStage<? extends U> other reports an error, an exception will be thrown directly and the method will not be executed.
Sample code:
public static CompletableFuture<Integer> divideNumber(int a, int b) {
return CompletableFuture.supplyAsync(() -> a / b);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> intFuture = divideNumber(10, 2)
.thenCombine(divideNumber(10, 5), (v1, v2) -> {
return v1 + v2;
});
CompletableFuture<Integer> exceptionFuture = divideNumber(10, 2)
.thenCombine(divideNumber(10, 0), (v1, v2) -> {
return v1 + v2;
});
log.debug("intFuture result:{}", intFuture.get());
log.debug("exceptionFuture result:{}", exceptionFuture.get());
}
The print log is:
23:41:52.889 [main] DEBUG pers.kerry.aservice.service.CFCombine - intFuture result:7
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
at pers.kerry.aservice.service.CFCombine.main(CFCombine.java:30)
Caused by: java.lang.ArithmeticException: / by zero
at pers.kerry.aservice.service.CFCombine.lambda$divideNumber$0(CFCombine.java:16)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
3.6. Summary and Comparison
The several methods described above are combined and compared horizontally.
1. thenApply, exceptionally, handle vs.
The difference between them lies in the incoming parameters of Function/BiFunction:
- thenApply: T -> U, which will be executed only when no error is reported.
- exceptionally: Throwable -> T, which will only be executed when an error is reported.
- handle: T,Throwable -> U, it will be executed whether an error is reported or not.
handle is a bit like a combination of thenApply and exceptionally.
2. thenApply, thenCompose vs.
They are all executed only when no error is reported, and finally return CompletableFuture, but the difference is the return parameter type in Function:
- thenApply:
Function<? super T,? extends U> fn)
- thenCompose:
Function<? super T, ? extends CompletionStage<U>> fn)
Therefore, it can be analogous to the difference between the map and flatMap methods in Stream, and the return parameter types in their Function:
- map:
Function<? super T, ? extends R> mapper
- flatMap:
Function<? super T, ? extends Stream<? extends R>
3. thenCombine
Unfortunately, thenCombine has no peers to compare in this category. Its function is to combine the values of two ComletableFutures and finally return a new ComletableFuture. Its horizontal comparison should be at allOf
. Just because it belongs to the category of Function, it is included.
4. Consumer method
Consumer method means that the input parameters of the following methods are all functional interfaces Consumer
or BiConsumer
. They are also executed after the completion of the previous CompletableFuture.
4.1. thenAccept
The thenAccept method is defined as:
public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
Features are:
- Input parameter Consumer T, if the former CompletableFuture does not report an error, after normal completion, the thenAccept method will be executed.
- There is no Throwable in the input parameter Consumer, so there is no exception handling. If the former CompletableFuture reports an error, an exception will be thrown directly and the method will not be executed.
Sample code:
public static CompletableFuture<Integer> divideNumber(int a, int b) {
return CompletableFuture.supplyAsync(() -> a / b);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Void> intFuture = divideNumber(10, 2)
.thenAccept((value) -> {
log.debug("intFuture result:{}", value);
});
CompletableFuture<Void> exceptionFuture = divideNumber(10, 0)
.thenAccept((value) -> {
log.debug("exceptionFuture result:{}", value);
});
Thread.sleep(1000);
}
The print log is:
00:15:21.228 [main] DEBUG pers.kerry.aservice.service.CFAccept - intFuture result:5
4.2. whenComplete
The whenComplete method is defined as:
public CompletableFuture<T> whenComplete(
BiConsumer<? super T, ? super Throwable> action)
Features are:
- Enter BiConsumer T, Throwable -> U, whether or not an error is reported, the whenComplete method will be triggered after the execution is completed.
- There is Throwable in the input BiConsumer, so it can handle exceptions. If the former CompletableFuture reports an error, this method will be triggered.
Sample code:
public static CompletableFuture<Integer> divideNumber(int a, int b) {
return CompletableFuture.supplyAsync(() -> a / b);
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> intFuture = divideNumber(10, 2)
.whenComplete((value, throwable) -> {
log.debug("whenComplete value:{}", value);
});
CompletableFuture<Integer> exceptionFuture = divideNumber(10, 0)
.whenComplete((value, throwable) -> {
if (throwable != null) {
log.error("whenComplete 出错啦");
}
});
log.debug("intFuture result:{}", intFuture.get());
log.debug("exceptionFuture result:{}", exceptionFuture.get());
}
The print log is:
00:19:27.738 [main] DEBUG pers.kerry.aservice.service.CFWhen - whenComplete value:5
00:19:27.746 [main] ERROR pers.kerry.aservice.service.CFWhen - whenComplete 出错啦
00:19:27.746 [main] DEBUG pers.kerry.aservice.service.CFWhen - intFuture result:5
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
at pers.kerry.aservice.service.CFWhen.main(CFWhen.java:37)
Caused by: java.lang.ArithmeticException: / by zero
at pers.kerry.aservice.service.CFWhen.lambda$divideNumber$0(CFWhen.java:20)
at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1590)
at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1582)
at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
4.3. Summary and comparison
1. thenAccept, whenComplete vs.
The difference between them includes the input parameters of the Function and the return value of the method:
- thenAccept: Input parameter: T, it will be executed only when no error is reported. Return CompletableFuture<Void>, if you use get() again, only null.
- whenComplete: Input parameter: Throwable, T, it will be executed whether an error is reported or not. Returns CompletableFuture<T>, which is the original CompletableFuture<T> before executing the whenComplete method.
In Function input parameters, their differences are similar to the previous thenApply、exceptionally、handle
. Some people say, why isn't there a method that only uses Throwable as a parameter? If you think about whenComplete
enough.
Other thenRun
There are input parameters in the front. If the subsequent methods still rely on the former and do not need to eat, you can try thenRun
.
5. Parallel processing method
The Function and Consumer mentioned above have a sequential execution relationship, because the later tasks depend on the results of the previous tasks. This chapter will talk about the processing method of parallel tasks.
5.1. Two parallel
5.1.1. both
Two tasks, if we want them to be executed, if we use a traditional future, we usually write it like this:
Future<String> futureA = executorService.submit(() -> "resultA");
Future<String> futureB = executorService.submit(() -> "resultB");
String resultA = futureA.get();
String resultB = futureB.get();
But in fact, in the methods introduced above, in addition to the thenCombine already mentioned, there are many variants of both methods that can be implemented:
CompletableFuture<String> cfA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture<String> cfB = CompletableFuture.supplyAsync(() -> "resultB");
cfA.thenAcceptBoth(cfB, (resultA, resultB) -> {});
cfA.thenCombine(cfB, (resultA, resultB) -> "result A + B");
cfA.runAfterBoth(cfB, () -> {});
5.1.2. either
The above problem, two tasks, if we want them to be completed as long as any one of them is executed, how to achieve it?
cfA.acceptEither(cfB, result -> {});
cfA.acceptEitherAsync(cfB, result -> {});
cfA.acceptEitherAsync(cfB, result -> {}, executorService);
cfA.applyToEither(cfB, result -> {return result;});
cfA.applyToEitherAsync(cfB, result -> {return result;});
cfA.applyToEitherAsync(cfB, result -> {return result;}, executorService);
cfA.runAfterEither(cfA, () -> {});
cfA.runAfterEitherAsync(cfB, () -> {});
cfA.runAfterEitherAsync(cfB, () -> {}, executorService);
Each of the above methods with either expresses the same meaning, which means that when one of the two tasks is completed, the specified operation is performed. The difference between their groups is also obvious. They are used to express whether the execution results of task A and task B are required, and whether the return value is required.
5.2. Multiple Parallelism
If we only consider the parallelism of two tasks, it is too limited. Here we will consider the parallelism of any number of tasks, the allOf
and anyOf
methods:
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs){...}
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {...}
Both methods are very simple, here is an example of allOf:
CompletableFuture cfA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture cfB = CompletableFuture.supplyAsync(() -> 123);
CompletableFuture cfC = CompletableFuture.supplyAsync(() -> "resultC");
CompletableFuture<Void> future = CompletableFuture.allOf(cfA, cfB, cfC);
// 所以这里的 join() 将阻塞,直到所有的任务执行结束
future.join();
Since allOf aggregates multiple CompletableFuture instances, it returns no value. This is also a disadvantage of it.
anyOf is also very easy to understand, that is, as long as any CompletableFuture instance is completed, see the following example:
CompletableFuture cfA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture cfB = CompletableFuture.supplyAsync(() -> 123);
CompletableFuture cfC = CompletableFuture.supplyAsync(() -> "resultC");
CompletableFuture<Object> future = CompletableFuture.anyOf(cfA, cfB, cfC);
Object result = future.join();
The join() method on the last line returns the result of the first completed task, so its generic type is Object, because each task may return a different type.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。