前書き
Springでは、@ Asyncアノテーションが付けられたアノテーション付きメソッドが非同期メソッドになります。これは、呼び出し元の現在のスレッドの外側にある別のスレッドで実行されます。これは、新しいスレッドを直接実行するのと同じです。
new Thread(()-> System.out.println( "new:" + Thread.currentThread()。getName()))。start();
なぜ非同期実行を使用するのですか?
多くの場合、時間のかかる非メインリンクメソッドを呼び出す必要があります。その実行結果は重要ではありません。後続のメソッドを実行する前に、ブロックして実行が終了するのを待つ必要はありません。同期方法では、タスクが順番に実行されるため、効率が低下します。タスクの合計時間は、各サブタスクによって消費された時間の合計です。
非同期実行を使用する利点は、タスクの実行効率が向上することです。サブタスクの実行時間を気にしない場合、タスクの合計時間はメインタスクの実行時間になります。
使用する
@Async
キーポイントに焦点を当てる:
- アノテーションはクラスをマークすることもでき、代表的なクラスのすべてのメソッドは非同期メソッドであることを指摘します
- 任意のパラメータータイプがサポートされていますが、メソッドの戻り値はvoidまたはFutureタイプである必要があります。Futureを使用する場合は、Futureインターフェイスを実装するListenableFutureインターフェイスまたはCompletableFutureクラスを使用して、非同期タスクとの対話を向上させることができます。非同期メソッドに戻り値があり、Futureタイプが使用されていない場合、呼び出し元は戻り値を取得できません。
- @Asyncは、特定のBeanName(つまり、スレッドプール、Executor / TaskExecutor)を指定してメソッドを実行し、Beanで実行されるクラスのクラスを表すすべてのメソッドを変更できます。メソッドレベルの装飾はクラスレベルよりも高くなります。装飾
- アノテーション@EnableAsyncは、呼び出し元の非同期メソッドクラスで構成する必要があります
/**
* Annotation that marks a method as a candidate for <i>asynchronous</i> execution.
* Can also be used at the type level, in which case all of the type's methods are
* considered as asynchronous.
*
* <p>In terms of target method signatures, any parameter types are supported.
* However, the return type is constrained to either {@code void} or
* {@link java.util.concurrent.Future}. In the latter case, you may declare the
* more specific {@link org.springframework.util.concurrent.ListenableFuture} or
* {@link java.util.concurrent.CompletableFuture} types which allow for richer
* interaction with the asynchronous task and for immediate composition with
* further processing steps.
*
* <p>A {@code Future} handle returned from the proxy will be an actual asynchronous
* {@code Future} that can be used to track the result of the asynchronous method
* execution. However, since the target method needs to implement the same signature,
* it will have to return a temporary {@code Future} handle that just passes a value
* through: e.g. Spring's {@link AsyncResult}, EJB 3.1's {@link javax.ejb.AsyncResult},
* or {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}.
*
* @author Juergen Hoeller
* @author Chris Beams
* @since 3.0
* @see AnnotationAsyncExecutionInterceptor
* @see AsyncAnnotationAdvisor
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Async {
/**
* A qualifier value for the specified asynchronous operation(s).
* <p>May be used to determine the target executor to be used when executing this
* method, matching the qualifier value (or the bean name) of a specific
* {@link java.util.concurrent.Executor Executor} or
* {@link org.springframework.core.task.TaskExecutor TaskExecutor}
* bean definition.
* <p>When specified on a class level {@code @Async} annotation, indicates that the
* given executor should be used for all methods within the class. Method level use
* of {@code Async#value} always overrides any value set at the class level.
* @since 3.1.2
*/
String value() default "";
}
基本的な使い方
インターフェースを定義する
テストメソッドは@Asyncアノテーションで装飾されています
public interface AsyncTestService {
@Async
void test();
}
インターフェイスを実装する
メソッドエントリはスレッド名を出力し、出力タスクはスリープ後に終了します
@Service
public class AsyncTestServiceImpl implements AsyncTestService {
@Override
public void test() {
System.out.println("测试异步调用,ThreadName:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("异步调用调用结束!");
}
}
クラスを開始
実行時間を記録し、スレッド名を出力し、テストメソッドを非同期で呼び出します。メインスレッドは結果を出力し続け、出力タスクはスリープ後に終了します。
@SpringBootApplication
public class SpringdemoApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(SpringdemoApplication.class, args);
long start = System.currentTimeMillis();
System.out.println("主线程执行,ThreadName:" + Thread.currentThread().getName());
AsyncTestService testService = (AsyncTestService) applicationContext.getBean("asyncTestServiceImpl");
testService.test();
try {
System.out.println("主线程输出:Hello World!");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程执行完毕,耗时:" + (System.currentTimeMillis() - start));
}
}
試験結果
印刷結果は次のとおりです。分析:メインスレッド実行出力 "メインスレッド実行、ThreadName:main"; testService.test()は非同期実行、メインスレッド出力 "メインスレッド出力:Hello World!";メインスレッドスリープ、子スレッドは非同期で実行されますテストメソッドは「Test非同期呼び出し、ThreadName:task-1」を出力します;テストメソッドのスリープ時間が短いため、「非同期呼び出し終了!」を出力します;最後にメインスレッドがウェイクアップして「メインスレッドの実行が完了しました、時間がかかります:2012」を出力します。テストメソッドが非同期で実行されていることがわかります。
メインスレッドの実行、ThreadName:main
メインスレッドの出力:Hello World!
非同期呼び出しのテスト、ThreadName:task-1
非同期呼び出しの終わり!
メインスレッドが実行され、時間がかかります:2012
同期比較
インターフェイスで@Asyncアノテーションをコメントアウトします。これは同期実行と同等であり、実行結果を確認します。
public interface AsyncTestService {
@Async
void test();
}
メインスレッドの実行、ThreadName:main
非同期呼び出しのテスト、ThreadName:main
非同期呼び出しの終わり!
メインスレッドの出力:Hello World!
メインスレッドが実行され、時間がかかります:7008
2012年から7008年にかけて時間が増えており(具体的な時間は数回測定して平均値をとっていますが、傾向は同じです)、効率に大きな影響を与えています。
カスタムスレッドプール
スレッドプールBeanを定義する
@EnableAsync(mode = AdviceMode.PROXY)
@Configuration
public class AsyncThreadPoolConfig {
/**
* 核心线程数(默认线程数)
*/
private static final int CORE_POOL_SIZE = 8;
/**
* 最大线程数
*/
private static final int MAX_POOL_SIZE = 20;
/**
* 允许线程空闲时间(单位:默认为秒)
*/
private static final int KEEP_ALIVE_TIME = 10;
/**
* 缓冲队列大小
*/
private static final int QUEUE_CAPACITY = 200;
/**
* 线程池名前缀
*/
private static final String THREAD_NAME_PREFIX = "async-task-pool-";
/**
* 当使用@Async注解时,需指定使用此线程池
*
* @return 线程池实例
*/
@Bean("asyncTaskExecutor")
public ThreadPoolTaskExecutor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(CORE_POOL_SIZE);
executor.setMaxPoolSize(MAX_POOL_SIZE);
executor.setQueueCapacity(QUEUE_CAPACITY);
executor.setKeepAliveSeconds(KEEP_ALIVE_TIME);
executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
executor.setThreadFactory(new ThreadFactory() {
// 线程计数器
private final AtomicInteger threadNumber = new AtomicInteger(0);
@Override
public Thread newThread(@NotNull Runnable runnable) {
Thread thread = new Thread(runnable, THREAD_NAME_PREFIX + threadNumber.getAndIncrement());
if (thread.isDaemon()) {
thread.setDaemon(false);
}
if (thread.getPriority() != Thread.NORM_PRIORITY) {
thread.setPriority(Thread.NORM_PRIORITY);
}
return thread;
}
});
// 线程池对拒绝任务的处理策略,拒绝执行且抛出异常
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 初始化
executor.initialize();
return executor;
}
}
インターフェースを定義する
スレッドプールのbeanNameとして@Aysncの値を指定します
public interface AsyncTestService {
@Async(value = "asyncTaskExecutor")
void test();
}
試験結果
実装は疲れており、起動は上記と同じです。テスト結果は次のとおりです。非同期スレッド名が設定されたルールに変更されていることがわかります。
メインスレッドの実行、ThreadName:main
メインスレッドの出力:Hello World!
非同期呼び出しのテスト、ThreadName:async-task-pool-0
メインスレッドが実行され、時間がかかります:2022
非同期呼び出しの終わり!
比較
カスタムスレッドプールのコンテキストでは、基本的な使用法と比較して多くの構成を行うことができます。一般的な構成は次のとおりです。
- カスタムコアスレッド
- CPUを集中的に使用:N + 1
- IO集約型:2 * N + 1
- カスタムスレッドプールのバッファキューサイズ
- このシナリオでタスク処理がより頻繁に行われる場合は、バッファーサイズを拡大する必要があり、その逆も同様です。
- 適切な飽和戦略の選択
-
AbortPolicy(デフォルトポリシー):RejectedExecutionExceptionをスローして、新しいタスクの処理を拒否します
-
DiscardOldestPolicy:最も古い未処理のタスク要求を破棄します
-
DiscardPolicy:タスクを実行せずに直接破棄します
-
CallerRunsPolicy:executeを呼び出すスレッドでこのコマンドを実行すると、入り口がブロックされます
スレッドプール実行戦略
- タスクが発生したときに、現在のワーカースレッドの数がコアスレッドの数より少ない場合、corePoolSizeは、タスクを実行するための新しいスレッドを作成します。
- 現在のワーカースレッドの数がコアスレッドの数以上の場合、タスクはブロッキングキューBlockingQueueに追加されます
- タスクをブロッキングキューに追加できない場合(キューがいっぱいの場合)、タスクを処理するための非corePoolスレッドを作成します
- 非corePoolスレッドが作成され、スレッドプールスレッドの数が最大スレッド数maxmumPoolSizeを超える場合、飽和戦略のRejectedExecutionHandler.rejectedExecution()が実行されます。