記事ディレクトリ
マルチスレッドの場合
ブロックキュー
コンセプト
ブロッキング キューは特別なキューです。また、「先入れ先出し」の原則に従います。
ブロッキング キューはスレッド セーフなデータ構造にすることができ、次の特性があります。
- キューがいっぱいの場合、キューに入り続けると、他のスレッドがキューから要素を取得するまでブロックされます。
- キューが空の場合、キューを終了し続けると、他のスレッドがキューに要素を挿入するまでブロックされます。
ブロッキング キューの典型的なアプリケーション シナリオは「プロデューサー コンシューマー モデル」で、これは非常に典型的な開発モデルです。
生産者消費者モデル
プロデューサー/コンシューマー モデルでは、コンテナーを使用して、プロデューサーとコンシューマー間の強い結合の問題を解決します。
プロデューサーとコンシューマーは相互に直接通信せず、ブロッキング キューを介して通信するため、プロデューサーはデータの生成後に待つ必要がありません。コンシューマによって処理された後、データはブロッキング キューに直接スローされ、コンシューマはプロデューサにデータを要求せず、ブロッキング キューから直接データを取得します。
- ブロッキング キューはバッファーに相当し、プロデューサーとコンシューマーの処理能力のバランスをとります。
- キューをブロックすると、プロデューサーとコンシューマーも分離されます。
メッセージ キュー: 特別なキュー。ブロッキング キューに基づいて「メッセージ タイプ」を追加するのと同等です。指定されたカテゴリに従って先入れ先出し。
上記のシナリオの場合: このとき、A はリクエストを B に転送し、B は処理後に結果を A に返します。この時点では、A が B を呼び出しているとみなすことができます。このとき、A と B の間の結合は比較的高くなります。B に問題がある場合は、A にも問題がある可能性があります。このとき、別のサーバーCを追加すると、Aのコードを再修正する必要があり、その過程でバグが発生しやすくなります。
上記のシナリオでは、生産者/消費者モデルを使用すると、カップリングを効果的に削減できます。
この時点で、A と B の間の結合は大幅に減少します。
A は B を知りません。A はキューだけを知っています。同様に、B は A を知らず、B はキューのみを知っています。AB のいずれかにバグがあったとしても、もう一方への影響は非常に小さいです。
プロデューサー コンシューマー モデルを書いてみましょう。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadDemo22 {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
//创建两个线程,来作为生产者和消费者
Thread customer = new Thread(()->{
while(true){
try {
Integer result = blockingQueue.take();
System.out.println("消费元素:"+result);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
customer.start();
Thread producer = new Thread(()->{
int count = 0;
while (true){
try {
blockingQueue.put(count);
System.out.println("生产元素:"+ count);
count++;
//控制500ms生产一个元素
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
producer.start();
}
}
標準ライブラリのブロックキュー
public interface BlockingQueue<E> extends Queue<E> {
...};
BlockingQueue はインターフェイスです。実際の実装クラスは LinkedBlockingQueue です。put
メソッドはキューのエントリをブロックするために使用され、take メソッドはキューの出口をブロックするために使用されます。BlockingQueue には、
offer、pol、peek などのメソッドもありますが、これらはメソッドにはブロック機能がありません
ブロッキングキューを自分で実装する
import java.util.concurrent.BlockingQueue;
//自己实现一个阻塞队列:
class MyBlockingQueue{
private int[] items = new int[1000];
private int head = 0;
private int tail = 0;
private int size = 0;
//入队列
public void put(int value) throws InterruptedException {
synchronized (this) {
while (size == items.length) {
//队列满了,此时要产生阻塞
this.wait();
}
items[tail] = value;
tail++;
if (tail >= items.length) {
tail = 0;
}
size++;
//唤醒 take 中的wait
this.notify();
}
}
//出队列
public Integer take() throws InterruptedException {
int result = 0;
synchronized (this){
while(size == 0){
this.wait();
}
result = items[head];
head++;
if(head >= items.length){
head = 0;
}
size--;
//唤醒 put中的wait
this.notify();
}
return result;
}
}
public class ThreadDemo23 {
public static void main(String[] args) throws InterruptedException {
/*MyBlockingQueue queue = new MyBlockingQueue();
queue.put(1);
queue.put(2);
queue.put(3);
queue.put(4);
int result = 0;
result = queue.take();
System.out.println("result = " + result);
result = queue.take();
System.out.println("result = " + result);
result = queue.take();
System.out.println("result = " + result);
result = queue.take();
System.out.println("result = " + result);*/
MyBlockingQueue queue = new MyBlockingQueue();
Thread customer = new Thread(()->{
while(true){
try {
Integer result = queue.take();
System.out.println("消费元素:"+result);
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
customer.start();
Thread produce = new Thread(()->{
int count = 0;
while(true){
System.out.println("生产元素:"+count);
try {
queue.put(count);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
count++;
}
});
produce.start();
}
}
タイマー
コンセプト
タイマー: 指定された時間が経過した後に、事前に準備されたメソッド/コードを実行します。
タイマーはソフトウェア開発における重要なコンポーネントです。特にネットワークプログラミングに関してはそうです。「目覚まし時計」に似ています。設定された時間が経過すると、指定されたコードが実行されます。
標準ライブラリのタイマー
Timer クラスは標準ライブラリで提供されます。Timer クラスの中心的なメソッドはスケジュールです。
スケジュールには 2 つのパラメータが含まれます。最初のパラメータは実行するタスク コードを指定し、2 番目のパラメータは実行にかかる時間を指定します (ミリ秒単位)。
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("运行定时器任务1!");
}
},3000);
timer.schedule();
このメソッドの効果は、タスクをタイマーに登録することです。タスクはすぐには実行されませんが、指定された時刻に実行されます。
タイマーを実装する
- 登録したタスクを指定時間内に実行させる
方法: タイマー内にスレッドを別途作成し、このスレッドが定期的に**(スキャンスレッド)**をスキャンさせ、タスクが時間計測されているかどうかを判定します。 - タイマーとはN個のタスクを登録できる**(スケジュールスレッド)**であり、N個のタスクは当初取り決められた時間に従って順番に実行されます。
これらの N 個のタスクは、優先ブロッキング キューを使用して保存する必要があります。(優先度があるのは、短い時間に応じて優先度を設定できるためです。このとき、キューの最初の要素がキュー全体で最初に実行されるタスクになります。このとき、上記のスキャンスレッドは、キューの最初の要素をチェックするため、キュー全体を走査する必要はありません (効率が大幅に向上します))
在这里插入代码片import java.util.concurrent.PriorityBlockingQueue;
//使用这个类表示一个定时器中的任务
class MyTask implements Comparable<MyTask>{
//要执行的任务内容
private Runnable runnable;
//任务在何时执行
private long time;
public MyTask(Runnable runnable,long time){
this.runnable = runnable;
this.time = time;
}
//获取当前任务的时间
public long getTime() {
return time;
}
//执行任务
public void run(){
runnable.run();
}
@Override
public int compareTo(MyTask o) {
//重写方法,按照时间排序。队首元素是时间最小的任务。
return (int)(this.time-o.time);
}
}
//定时器
class MyTimer{
//扫描线程
private Thread t = null;
//使用一个优先级队列。来保存任务
private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
//使用这个对象来进行加锁/等待通知
private Object locker = new Object();
public MyTimer(){
t = new Thread(){
public void run() {
while (true) {
try {
//取出队首元素,检查看看队首元素任务是否时间到了
//如果时间没到,就把任务塞回队列中
//如果时间到了,就把任务进行执行
synchronized (locker) {
MyTask myTask = queue.take();
long curTime = System.currentTimeMillis();
if (curTime < myTask.getTime()) {
//时间没到,塞回队列
queue.put(myTask);
//在put之后,进行一个wait
locker.wait(myTask.getTime() - curTime);
} else {
//时间到了,执行任务
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
}
public void schedule(Runnable runnable,long after){
MyTask task = new MyTask(runnable,System.currentTimeMillis()+after);
queue.put(task);
synchronized (locker) {
locker.notify();
}
}
}
public class ThreadDemo25 {
public static void main(String[] args) {
MyTimer myTimer = new MyTimer();
myTimer.schedule(new Runnable(){
public void run(){
System.out.println("任务1");
}
},2000);
myTimer.schedule(new Runnable(){
public void run(){
System.out.println("任务2");
}
},1000);
}
}
操作結果:
スレッドプール
スレッド プールの存在の重要性: プロセスを使用して並行プログラミングを実現します。重すぎる。このときスレッドが導入され、スレッドは「軽量プロセス」とも呼ばれます。スレッドの作成はプロセスの作成よりも効率的であり、スレッドの破棄はプロセスの破棄よりも効率的であり、スレッドのスケジューリングはプロセスのスケジューリングよりも効率的です。現時点では、多くの場合、マルチスレッドを使用することでプロセスを置き換えて、同時プログラミングを実現できます。しかし、同時実行性の度合いが増加するにつれて、パフォーマンス要件も増加します。スレッドの軽量性も低下しました。さらに効率を向上させ
たい場合は、次の 2 つの方法があります。
- 軽量スレッド - コルーチン/ファイバー
- スレッド プールを使用して、スレッドの作成/破棄のオーバーヘッドを削減します。
つまり、使用する必要のあるスレッドを事前に作成し、プールに入れます。後で使用する必要がある場合は、プールから直接取得してください。使い終わったらプールに入れてください。
上記の操作は、作成/破棄よりも効率的です。
スレッドの作成/破棄はオペレーティング システム カーネルによって行われます。プールからの取得とプールへの返却は、操作のためにカーネルに渡すことなく、独自のコードで実現できます。
標準ライブラリのスレッドプール
ExecutorService pool = Executors.newFixedThreadPool(10);
上記の操作では、クラスの静的メソッドを使用してオブジェクトを直接構築します。(新規を非表示)
このような方法を「ファクトリーメソッド」と呼びます。
このファクトリメソッドを提供するクラスを「ファクトリクラス」と呼びますが、このコードではファクトリパターンのデザインパターンを使用しています。
工場パターン
ファクトリ パターン: コンストラクターの代わりに通常のメソッドを使用してオブジェクトを作成します。
たとえば、
次のクラスがあります。
class Point{
public Point(double X,double Y){
};
public Point(double R,double A){
};
}
コンストラクターは 1 つしか存在できないため、この時点でコードは間違っています。ただし、テーブルを表現する 2 つの方法を実装したいと考えています。この場合、ファクトリーパターンを使用できます。
class PointFactory{
public static Point makePointXY(double X,double Y){
};
public static Point makePointRA(double R,double A){
};
}
このとき、上記の問題は解決される。
スレッド プールで提供されるメソッド: submit;
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int n = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello"+n);
}
});
}
}
注意する必要があるのは、上記のコードの変数キャプチャの問題です。そのため、印刷時に i? の代わりに変数 n が別途作成されます。
i は、メインスレッド (メインスレッドのスタック上) のローカル変数です。メインスレッドがこのコード ブロックを実行すると、このコード ブロックは破棄されます。スコープの違いを避けるため、以降の実行時に i が破棄されます。つまり、run メソッドが実行スタックにメインスレッドの i をコピーするという変数のキャプチャがあります。変数キャプチャでは、最終的に変更された変数のみをキャプチャできます (JDK1.8 より前)。JDK1.8 以降は規格が緩和され、コード内で変数が変更されていない限り、変数もキャプチャできるようになりました。
上記のコードでは、i が変更されているため、キャプチャできません。そして、n は変更されていません。最終的な変更はありませんが、キャプチャすることもできます。
ThreadPoolExecutor(); 構築メソッドのパラメータの詳細な説明 (強調)
ここでパラメータ間の関係を理解しやすくするために、ここに会社があると仮定して、人生における例を使用して類推して理解します。
- corePoolSize は、会社の正式な従業員であるコア スレッドの数を示します。
最適なコア スレッドの数はどれくらいですか? CPU に N 個のコアがあると仮定した場合、最適なコア スレッドの数はどれくらいですか? 2N ですか? 1.5N ですか? 具体的な数を挙げることができれば、それは間違いですコアスレッドの数は
状況やビジネスシナリオによって異なり、絶対的な標準値はありません。
- MaximumPoolSize は、コア スレッドの数と非コア スレッドの数の合計であるスレッドの最大数を示します。会社の正式な従業員と雇用されたゼロ時間労働者 (非コア スレッド) は、既存の作業が完了すると、ゼロを採用します
。タイムワーカーが作業を手伝います。 - keepAliveTime 非コア スレッドが新しいタスクを待機する最長時間。この時間が経過すると、スレッドは破棄されます。これは、ゼロ時間ワーカーの最長フィッシング時間に相当します。同社はアイドラーをサポートしていません
。・時間労働者は長時間仕事がない、やらなければ解雇するという全体的な戦略として、正社員には最低保障を確保し、派遣社員には動的調整を図るというものである。 - 単位 上記パラメータの時間単位。
- workQueueスレッドプールのタスクキュー(ブロッキングキュー)は、submitメソッドを通じてタスクをキューに登録します。
- threadFactory スレッド ファクトリ、スレッド作成スキーム。
- ハンドラー拒否戦略では、スレッド プールのタスク キューがいっぱいになった場合にタスクを追加する方法を説明します。
Java 標準ライブラリでは、次の 4 つの拒否戦略が提供されています。
修飾子とタイプ | クラスと説明 |
---|---|
静的クラス | ThreadPoolExecutor.AbortPolicy タスクが多すぎてキューがいっぱいの場合、例外 RejectedExecutionException が直接スローされます。 |
静的クラス | ThreadPoolExecutor.CallerRunsPolicy タスクが多すぎる場合、キューがいっぱいになり、追加のタスクが追加され、誰がタスクを実行するかを決定します。 |
静的クラス | ThreadPoolExecutor.DiscardOldestPolicy タスクが多すぎてキューがいっぱいの場合は、最も古い未処理のタスクを破棄します。 |
静的クラス | ThreadPoolExecutor.DiscardPolicy タスクが多すぎてキューがいっぱいの場合は、余分なタスクを破棄します。 |
スレッドプールを実装する
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool{
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
//n表示线程的数量
public MyThreadPool(int n){
//创建线程
for (int i = 0; i < n; i++) {
Thread t = new Thread(()->{
while(true){
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
//
public void submit(Runnable runnable) {
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadDemo27 {
public static void main(String[] args) {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int n = i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello " + n);
}
});
}
}
}