マルチスレッド(4)


前書き:

これまでの記事で、マルチスレッドに関する基本的な知識を学びました. マルチスレッドの理解を深めるために、マルチスレッドに関するいくつかのコードケースを書きましょう. さらに、これらのケースを通じて他の知識を補うことができます.

1.シングルトンモード


シングルトンパターン:デザインパターンの一種。


ここで、デザインパターンとは何かという問題があります。


例: チェス (チェス、囲碁、バックギャモンなど) をプレイする場合、チェスをもっと上手にプレイしたい場合は、間違いなくゲームの記録を研究して研究します. 結果が出ます. ここでは、チェスの記録に従ってチェスをプレイします, 基本的

にチェスの強さは悪くない.

同じ理由: コンピュータ界ではレベルにばらつきがある. 新人 (特に私) がこのコードをそれほどひどく書かないようにするために, デザインパターンと呼ばれるチェスレコード. .


したがって、設計パターンはいくつかの典型的なシナリオを対象としており、いくつかの典型的なソリューションを提供しています.

設計パターンに精通し、設計パターンに従って開発できる限り、コードは現時点で悪く書かれることはありません.


シングルトン パターンとは何かを簡単に理解して、それについて学びましょう。最も一般的に使用される 2 つのデザイン パターン

1. シングルトン パターン


2.ファクトリーモード


シングルトン パターン: 単一のインスタンス (オブジェクト)


シナリオによっては、特定のクラスのインスタンスを 1 つしか作成できず、複数のインスタンスを作成すべきではない場合がありますが、

シングルトン モードを使用した後、現時点で複数のインスタンスを作成することは困難です。


注: このような要件はシングルトン モデルに依存するものではなく, 紳士協定が成立するかどうかに依存します. たとえば, 古代では, 皇帝はハーレムに 3,000 人の美人を持つことができました. Marry you one (紳士協定),現代では複数の妻を娶ると重婚となる。現時点では、紳士協定だけでなく、法的規制も含まれています。


私たちのシングルトン モードは、上記の要求シナリオに対して必須の保証です. Java の既存の構文を巧みに使用することにより、特定のクラスのインスタンスを 1 つだけ作成できます. この効果, (複数のインスタンスがある場合に誤って作成すると、エラーが発生しますコンパイル時に報告されます)。


復習: jdbc を学ぶとき, DataSource のようなクラスは実際にはシングルトン モードを使用するのに非常に適しています. DataSource はデータがどこにあるかを記述するだけです. 私たちのデータは 1 つから来ますMySQL. このとき, 複数の DataSource を作成すると同じものを参照するので,私たちのjdbc

では、DataSource はシングルトン モードを使用します。


以下に続けて、シングルトン パターンの 2 つの典型的な実装があります。


1.ハングリーマンモード


2.レイジーモード


コードを通してこれら2つの実装を学びましょう


ハングリーマン モード

ハングリーマン モード: インスタンスはクラス ローディングの段階で作成されます (クラス ローディングは比較的早い段階です)、この効果は人々に特に切迫感を与えます. この感覚を通じて、比較イメージが作成されます ハングリーマン名前モード。

ここに画像の説明を挿入

レイジーモード


例:4人家族がお昼にご飯を食べて4杯残っていたが急いで洗わず、次に食べる時は何人で何杯洗ったか。

例: 夕食を食べている人が 2 人しかいない場合、2 つの皿を洗います. これは怠け者モードであり、必要な場合にのみオブジェクトを作成します.

ここでの食器洗い空腹モードの例は、食事の直後に食器を洗い、次に使用する必要があるときまで待たないことです。

ここに画像の説明を挿入


一般に、コンピューターではレイジー モードの方がハングリー モードよりも優れています。


続けましょう。上記の遅延モードには実際に問題があります。何が問題なのか見てみましょう。


記事のタイトル、マルチスレッドを見ましたか? レイジー モードとハングリー モードのマルチスレッド モードで getInstance (オブジェクトの取得) を呼び出すことがスレッド セーフであるかどうかを検討しましたか?


上記のスレッド セーフの問題

思い出し

ください






ハングリー モード: マルチスレッド呼び出しは読み取り操作のみを含むため、マルチスレッド スレッドではスレッド セーフです。

ここに画像の説明を挿入


レイジー モード: レイジー モードには、読み取り操作と書き込み操作 (オブジェクトの作成) の両方が含まれ、この時点でスレッド セーフの問題が発生します。

ここに画像の説明を挿入


絵を描いて、レイジーマンモードがスレッドセーフである理由を説明しましょう。

ここに画像の説明を挿入


分析の結果、レイジーマン モードにはスレッド セーフの問題があることがわかりました。次に、現在のスレッド セーフの問題をロック操作で解決します。

ここに画像の説明を挿入

この時点で、ロック操作は終了しました。終了したと思いますか?実際、コードにはまだ問題があり、パフォーマンスの問題があります。

ここに画像の説明を挿入


ここに書きながらお聞きしたいのですが、このコードに何か問題はありますか?


ここで大騒ぎするつもりはありませんが、実際には、メモリの可視性と命令の並べ替えという 2 つの問題があります。


メモリの可視性:

ここに画像の説明を挿入


注文の再配置:

ここに画像の説明を挿入


ここのシングルトンモードでは、怠け者モードとハングリーマンモードを学習しました. 次に、別のケースであるブロッキングキューを学習しましょう.

2.ブロッキングキュー


リコール: キューの特性は先入れ先出しであり、ここでのブロッキング キューもキューであるため、この特性も満たしています。


さらに: 一部の特別なキューは、必ずしも先入れ先出し機能に従っているとは限りません. たとえば、優先度キューはPriorityQueue先入れ先出しではなく、ルートが大きいかどうかに応じて最小または最大になります.杭または小さな根杭

また、ブロッキング キューも特別なキューであり、先入れ先出し機能を満たしますが、特別な機能があります。


機能: ブロッキング


1. キューが空の場合、別のスレッドが要素をキューに追加するまで、キュー操作はブロックされます (この時点ではキューは空ではありません)。


2. キューがいっぱいの場合、別のスレッドがキューから要素を削除するまで、キュー操作もブロックされます (この時点ではキューはいっぱいではありません)。


拡張: メッセージ キューも特殊なキューです。これは、指定されたカテゴリに従って先入れ先出しを実行するために、ブロッキング キューに基づいて「メッセージ タイプ」を追加することと同じです。


例えば:

ここに画像の説明を挿入


理解:

さらに、メッセージキューの性質上、より香りがよいため、大物はそのようなデータ構造をプログラムとして単独で実装し、このプログラムはネットワークを介して他のプログラムと通信できます。

この時点で、メッセージ キューはサーバーのグループ (分散) に個別に展開でき、ストレージ容量と転送容量が大幅に向上します。

このようなメッセージ キューは大規模なプロジェクトに多く見られ、現在、メッセージ キューは mysql や redis と比較される重要なコンポーネント「ミドルウェア」になっています。

rabbit mqこれはメッセージ キューの典型的な実装です.

他にも多くの実装があります. active mq, rocket mq, kafka... は、業界でよく知られているメッセージ キューです。


前述のように、ブロッキング キューに合わせてメッセージ キューが変更され、「メッセージ タイプ」が追加されるため、メッセージ キューを明確に理解したい場合は、まず「ブロッキング キュー」を理解する必要があります。


プロデューサーコンシューマー モデルを見てみましょう


プロデューサ/コンシューマ モデルは、ブロッキング キューのブロッキング特性に基づいて実装されます。


例:家族が餃子を作る.

餃子を作るには、麺棒を使って餃子の皮を伸ばし、肉の詰め物を餃子の皮に包む. この2つのステップ.


この時の代表的な包み方には、

1. 一人一人が餃子の皮を巻いて餃子を作る

2. 一人が餃子の皮を巻くのを担当し、他の何人かが包むのを担当する. 巻くのを担当する人餃子の皮は、餃子の皮を丸めて天幕(餃子の皮を載せるためのもの)の上に置き、餃子を作る人が天幕から直接取り出せます。


現時点では、最初の方が良いと思いますか、それとも2番目の方が良いと思いますか?

明らかに2番目:


1つ目:めん棒が1本しかないため、全員が競い合い、結果的にめん棒を取れなかった人を待つことになり、効率に影響する. これは、ブロックしてロックを取得していないのを待っているようなものです.


2つ目は、自分の仕事を専門に行う人がいるため、競争がありません。効率は当然高く、これはコンシューマー・プロデューサー・モデルです


ここ で 餃子 の 皮 を 巻く 人 が生産 者、 餃子 を 作る 責任 者 が消費 者相当し 、 幕 は取引所(生産 者と 消費 者 の 連絡 と 交流 の 架け橋) に 相当 する .

遅すぎるとコンシューマーを待たせる可能性があり、消費が遅すぎるとプロデューサーを待たせる可能性があります。

消費者と生産者のモデルが何であるかがわかったところで、生産者と消費者のモデルが私たちのプログラムにもたらす 2 つの非常に重要な利点を見てみましょう。


1. 投光器と受光器のデカップリング


ここでのデカップリングとは、あなたが聞いたことがあるはずの高結束と低結合と比較して、結合を減らすプロセスです。


高い凝集度: 同じタイプのデータをまとめて、スペースを節約し、スペースの使用率を向上させます。

低結合: プログラム間の接続ができるだけ少ないことが望まれます


カップリングを減らすには?

最初に例を見てみましょう: サーバー間の呼び出し (開発における典型的なシナリオ)

ここに画像の説明を挿入


消費者と生産者のモデルを使用して結合を減らします。

ここに画像の説明を挿入


最初の利点を読んだ後、2 番目の利点を見てみましょう:システムの安定性を確保するためにピークをカットし、谷を埋める


例:

ここに画像の説明を挿入

ここに画像の説明を挿入


理論的な知識はここまでです。コードを通して学びましょう。ブロッキング キュー


ここでは、主に2つの側面から学びます


1. 標準ライブラリが提供するブロッキング キューを使用します。


2. シンプルなブロッキング キューが実装されます


標準ライブラリのブロッキング キュー

ここに画像の説明を挿入


上記のことを知っていれば、標準ライブラリのブロッキング キューに基づいてプロデューサー/コンシューマー モデルのコードを書くことができます。

ここに画像の説明を挿入


コードを添付します。


    public static void main(String[] args) {
    
    
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();

        // 创建两个线程, 来作为生产者和消费者

        // 1. 消费者
        Thread customer = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    while(true){
    
    
                        Integer result = blockingQueue.take();
                        System.out.println("消费元素 : " + result);
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });

        customer.start();

        // 2. 生产者
        Thread producer = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                int count = 0;
                while (true) {
    
    
                    try {
    
    
                        count++;
                        blockingQueue.put(count);
                        System.out.println("生产元素 : " + count);
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        });
        producer.start();
    }


最も単純なプロデューサー/コンシューマー モデルを作成した後、ブロッキング キューについてより明確に理解する必要があります。次に、ブロッキング キューを実装しましょう。


ここでの単純さと直感のために、ここでシミュレートされたブロッキング キューはジェネリックを持たず、int 型を直接使用します。


準備:

ここに画像の説明を挿入


ブロッキング キューを実装したいので、最初に通常のキューを実装する必要があります。

この通常のキューは、配列または連結リストに基づくことができます。

実際、データ構造を学習すると、すでに実現されています: Queue-Queue

連結リストによる実装は比較的簡単なので、興味のある方は試してみてください. この記事では配列による実装を採用しています.

主な実装:

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) {
    
    

    }

    // 出队列
    public Integer take() {
    
    

    }

    // 阻塞队列 无序提供获取队首元素的方法
}


次の実践的な実装:


1.共通キューを実装する

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) {
    
    
        if (size == items.length) {
    
    
            // 队列满了 , 不能插入
            return;
        }
        items[tail] = value;
        tail++;

        // 针对 tail 处理 两种做法

        // 1. tail = tail % items.length
        // 上面已经 tail++ ,所以 这里不是 +1 取膜

        // 2.
        if (tail >= items.length) {
    
    
            tail = 0;
        }

        // 最后别忘了 了 size
        size++;
    }

    // 出队列
    public Integer take() {
    
    

        if (size == 0) {
    
    
            // 队列空不能出队列
            return null;
        }

        int result = items[head];
        head++;
        if (head >= items.length) {
    
    
            head = 0;
        }
        size--;
        return result;
    }

    // 阻塞队列 无序提供获取队首元素的方法


}

public class Test {
    
    
    public static void main(String[] args) {
    
    
        MyBlockingQueue queue = new MyBlockingQueue();
        queue.put(1);
        queue.put(2);
        queue.put(3);
        queue.put(4);
        int 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);
    }

}


結果:

ここに画像の説明を挿入


2. 通常のキューが実装された後、wait と notify を追加して、ブロッキング関数を追加できます。


さらに、ブロッキング機能は、マルチスレッド環境でキューを使用する必要があることを意味します。


したがって、ここではスレッドセーフに注意を払う必要があり、ロック操作を追加する必要があります (同期を使用)。

ここに画像の説明を挿入


ここに書いている限り、私たち自身のブロッキングキューはほぼ完成していますが、まだ少し欠陥があります。修正しましょう

ここに画像の説明を挿入

最後に私たちのコード:

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) {
    
    
                // 队列满了 , 不能插入
//                return;
                // 队列满了 ,插入元素阻塞等待
                this.wait();
            }
            items[tail] = value;
            tail++;

            // 针对 tail 处理 两种做法

            // 1. tail = tail % items.length
            // 上面已经 tail++ ,所以 这里不是 +1 取膜

            // 2.
            if (tail >= items.length) {
    
    
                tail = 0;
            }

            // 最后别忘了 了 size
            size++;
            // 此时队列添加元素, 不为空 ,可以唤醒为空时的
            // 出队列的阻塞等待
            this.notify();
        }
    }

    // 出队列
    public Integer take() throws InterruptedException {
    
    

        synchronized (this) {
    
    
            while (size == 0) {
    
    
                // 队列空不能出队列
//                return null;

                // 队列为空想要出元素 就需要阻塞
                this.wait();
            }

            int result = items[head];
            head++;
            if (head >= items.length) {
    
    
                head = 0;
            }
            size--;
            // 此时 相当于出了一个元素,那么队列满的时候阻塞就可以唤醒
            this.notify();
            return result;
        }
    }

    // 阻塞队列 无需要提供获取队首元素的方法


}


独自のブロッキング キューにプロデューサー コンシューマー モデルを使用してみましょう。


public class Test {
    
    


    public static void main(String[] args) {
    
    
        MyBlockingQueue queue = new MyBlockingQueue();

        // 1. 生产者
        Thread customer = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                while (true) {
    
    
                    try {
    
    
                        while (true) {
    
    
                            Integer result = queue.take();
                            System.out.println("消费元素 : " + result);
                        }
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }

            }
        });
        customer.start();

        // 2. 生产者
        Thread producer = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                int count = 0;
                while (true) {
    
    
                    try {
    
    
                        count++;
                        queue.put(count);
                        System.out.println("生产元素 : " + count);
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        });
        producer.start();
    }


結果:

ここに画像の説明を挿入


ブロッキング キューを読み取った後、3 番目のケースであるタイマーについて学びましょう。

3. タイマー


タイマーは目覚まし時計に似ており、指定された時間が経過すると、準備されたメソッド/コードの実装が実行されます。


2時間ロールしてからゲームをしなければならないのと同じで、2時間後、この時間にゲームをする必要があり、タイマーはこのようになっています。


另外 : 定时器也是开发中常用的组件 , 尤其是在网络编程中 , 比如我们在访问某个网站的时候,突然网不好,一直转圈圈,刷新不出页面,当转了一个指定的时间就会弹出网络不好的页面.这里相当于有一个等待的超时时间,当过了这个时间,就不会在等待了,直接返回一个访问失败,或网站不见了. 这里就运用了定时器 ,当执行超过某个时间,就返回错误信息。


这里定时器和阻塞队列类似,标准库 同样也给我们提供了 .

ここに画像の説明を挿入


下面就来使用一下 :

ここに画像の説明を挿入


使用完库里面的定时器,下面我们就来自己实现一个定时器 .


这里主要完成 一下两点


1.让被注册的任务 , 能够在指定时间, 被执行


2.一个定时器是可以注册 N 个任务的 , N个任务会按照最初约定的时间,按照顺序执行


实现 思路 :

1.单独在定时器内部, 弄一个线程,让这个线程周期性的扫描,判定任务是否到时间了,如果到时间了,就执行,没有到时间就在等等 .

2.既然我们需要注册 N 个任务,这里我们就需要使用一个数据结构来保存任务 , 周期的扫描 这个数据机构 , 那个任务时间到了,就执行那个任务。


这里的核心 就两点

1.有一个扫描的线程,负责判断时间是否到了(是否执行任务)

2.有一个存放任务的数据结构


那么大家想一想这个数据结构 我们要使用那个呢 ?


链表 , 哈希表 ,栈 , 队列 还是优先级队列呢 ?


仔细想一想 , 优先级队列是不是就是我们需要的 。

优先级队列, 存放我们的任务, 任务是有执行时间的, 这里时间越靠前 ,就越先执行 . 这里优先级队列(堆) 就可以采用最小堆 ,让时间最小的排在前面,此时扫

描就可以扫描队首元素, 不必要扫描整个队列了(最小堆 , 最小的放在根部,如果队首元素还没到执行时间内, 后续元素更不可能到时间)


另外 : 我们的优先级队列会在多线程环境下使用 ,很明显 ,调用 schedule 是一个线程, 扫描是另外一个线程 (两个线程都可能对我们的队列进行修改, 一个是入 一个是出),此时 我们就需要考虑线程安全问题 , 这里就可以采用加锁操作来保证线程安全 。


另外一种方法 : 我们也可以使用 标准库里面实现好的 PriorityBlockingQueue 带优先级的阻塞队列


分析完这么多 ,下面来看代码 :


主要逻辑
ここに画像の説明を挿入


解决错误一 : 确定优先级

ここに画像の説明を挿入


解决错误二 :忙等问题

忙等 : 简单来说 ,再原本要休息的时候 确没有休息到,还一直再干无意义的事情.


比如 泡面,泡面需要3分钟,但一直盯着泡面 ,此时泡面会提前煮开吗? 显然不会,有这三分钟 看泡面番不香吗 .

这里忙等 就会一直吃 cpu的资源,但不会做实际性的工作 , 虽然忙等 会浪费资源,但并不是忙等就是不好的, 再某些特定的场景下忙等可能就是一个好的 选着


下面就来,修改代码 , 让它不要进行忙等了,而是进行 阻塞式 等待


这里 就可以使用 sleep 或 wait 。


这里就有一个小问题 , 此时我们的等待需要等待多久呢 ?


举例 : 当前是 13:00 队首元素是 14 :00 此时就需要等待 1个小时 .


上面这个举例 ,我们能够明确的知道等待时间,那么是不是就使用sleep 呢 ?

答案是 不的, 因为我们这里只是举出了一个例子, 知道其中一个等待时间,如果此时再添加一个 13点 40 的任务呢 ? 此时队首元素是不是就是 13 点 40 的任务了(最小堆 ,会指定调整队首元素保证是最小的, 之前给定了比较规则,根据时间来进行比较) .

所以 使用 sleep 就不太合理, 这里就需要使用 wait() ,

我们使用 wait 可以说是非常方便的 , 因为wait 可以随时唤醒, 当有新的任务来时 (此时调用 schedule 添加新的任务) ,就可以通过 notify 唤醒一下,重新检查一下时间, 重新计算要等待的时间, 并且 wait 也提供了一个 带有 超时时间 的版本 .


这里带有超时时间 的wait 就可以保证


1.当新的任务来了,随时通过notify 唤醒

2.如果没有新任务,则最多等到之前旧任务中的最早任务时间到(队首 :任务的时间到了)就被唤醒


下面就来修改代码 :
ここに画像の説明を挿入


此时 上面的两个问题解决了, 其实代码中还有一个比较严重的问题 。


这个问题 和 线程安全 / 随机调度 密切相关 :

ここに画像の説明を挿入


附上代码 :

package T_J4.T_1_5;


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<>();

    // 指定了两个参数  1. 任务内容 , 2. 任务在多少毫秒之后执行
    public void schedule(Runnable runnable, long after) {
    
    

        // 这里的 after 形如 1000 毫秒这样的, 这里就需要获取当前时间戳 加上这个多少秒后执行的时间
        MyTask task = new MyTask(runnable, System.currentTimeMillis() + after);

        // 将我们的任务放到 优先级队列中
        queue.put(task);
        synchronized (this) {
    
    
            this.notify();
        }
    }

    public MyTimer() {
    
    
        t = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                while (true) {
    
    
                    // 1. 取出队首元素 ,检查队首元素任务是否到了时间(是否需要执行)
                    try {
    
    
                        synchronized (this) {
    
    
                            MyTask myTask = queue.take();

                            long curTime = System.currentTimeMillis();

                            if (curTime < myTask.getTime()) {
    
    
                                // 此时任务 还没到点, 先不执行
                                //  将任务重新放入 队列中
                                queue.put(myTask);

                                // 再 put 后 ,进行 一下 wait 操作 ,
                                this.wait(myTask.getTime() - curTime);

                            } else {
    
    
                                // 时间到了 执行任务
                                myTask.run();
                            }
                        }

                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }

                }
            }
        });
        t.start();
    }
}

public class Test3 {
    
    
    public static void main(String[] args) {
    
    
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("鸡哥");
            }
        }, 2000);
        myTimer.schedule(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("打篮球");
            }
        }, 1000);
    }
}


看完了 定时器, 下面来看最后第一个代码案例,线程池 .

4.线程池


回忆一下 线程存在的意义 : 是不是因为使用进程来实现并发编程,太重了 (频繁创建销毁进程 ,开销比较大),此时就引入了 线程, 线程也叫做 轻量级进程 , 创建线程比创建进程更高效, 销毁线程比销毁进程更高效 , 调度线程比调度进程更高效 。

正因如此,使用多线程就可以再很多时候代替进程来实现并发编程 。

但是 随着并发程度的提高 ,随着我们对于性能要求标准的提高 , 此时发现 线程创建好像也没有那么轻量了 .


那么想要更进一步的提高这里的效率, 就有两种办法 :

1.搞一个 “轻量级线程” —> 协程 或者叫 纤程 这个东西,目前还没有被加入到 java 标准库中 ,这里不展开 。

在 Go 中就内置了 协程 , 因此Go开发并发编程程序就有一定的优势.


2. スレッド プールを使用して作成/破棄のオーバーヘッドを削減する


スレッドプールは、使用する必要のあるスレッドを事前に作成してプールに入れ、後で使用する必要がある場合はプールから直接取得し、使用後は破棄せずに直接プールに入れます。 /Destroy の方が効率的です。


スレッドを作成して破棄するよりも、スレッドをスレッド プールに入れ、スレッド プールからスレッドを取得する方が効率的なのはなぜですか?


ここでは、オペレーティング システムのカーネル モードとユーザー モードの概念を理解する必要があります。

ここに画像の説明を挿入


概念を読んだ後、Java標準ライブラリが提供するスレッドプールを使用しましょう

ここに画像の説明を挿入


10 個のスレッドでスレッド プールを構築すると、これらのスレッドを調整して作業を支援できます。


ここをどうアレンジするか。


スレッド プールが提供する重要なメソッド submit を使用する必要があります. submit メソッドを使用すると、スレッド プールに promise を送信してタスクを実行できます.

ここに画像の説明を挿入


別のポイントを追加します。

ここに画像の説明を挿入


この時点で、スレッド プールの最も基本的な使用法を簡単に理解できます. ここでは、ファクトリ メソッドを使用してスレッド オブジェクトを作成し、サブミットを使用してスレッド プールにタスクを追加します。


Executors他のスタイルのスレッド プールを提供するファクトリクラスを見てみましょう。


図1:

ここに画像の説明を挿入


図 II:

ここに画像の説明を挿入


この時点で、いくつかの理論的な知識は終わりました。単純なスレッド プールをシミュレートして実装しましょう。


これは、少なくとも 2 つの大きな部分を持つスレッド プールです。

1. ブロッキング キュー、タスクの保存

2. ワーカー スレッドの実行を約束

ここに画像の説明を挿入


コードを添付します。


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPoll {
    
    

    // 此处不涉及到 时间
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    // n 表示线程的数量
    public MyThreadPoll(int n) {
    
    

        // 在构造方法中创建处线程 ---> 创建 n 个线程, 每个线程的 run 方法会取出任务 执行任务

        for (int i = 0; i < n; i++) {
    
    
            Thread t = new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    while (true) {
    
    
                        try {
    
    
                            Runnable runnable = queue.take();
                            runnable.run();
                        } catch (InterruptedException e) {
    
    
                            e.printStackTrace();
                        }
                    }
                }
            });
            t.start();
        }
    }

    // 注册任务给线程池
    public void submit(Runnable runnable) {
    
    
        try {
    
    
            queue.put(runnable);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }


}

public class Test6 {
    
    


    public static void main(String[] args) {
    
    
        MyThreadPoll pool = new MyThreadPoll(10);

        for (int i = 0; i < 1000; i++) {
    
    
            int n = i;
            pool.submit(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    System.out.println("任务执行序号 : " + n);
                }
            });
        }
    }
}

この時点で、マルチスレッドに関するいくつかのコード ケースが完成しました。

次のプレビュー: マルチスレッドに関するいくつかのインタビューの質問 + あまり使用されていないマルチスレッド コンポーネント...

おすすめ

転載: blog.csdn.net/mu_tong_/article/details/128600197