Javaマルチスレッドと高い同時実行性の打ち負かし-同期コンテナーと並行コンテナー

この記事は、Javaマルチスレッドと高い同時実行性の知識を学ぶときに作成されたメモです。

この部分には多くのコンテンツがあり、内容に応じて5つの部分に分かれています。

  1. マルチスレッドの基本
  2. JUCの記事
  3. 同期コンテナと同時コンテナ
  4. スレッドプール
  5. MQの記事

これは同期コンテナと同時コンテナです。

目次

1コレクション

1.1リスト

1.2セット

1.3マップ

2ブロッキングキュー

2.1ブロッキングキューを操作するための4セットのAPI

2.1.1例外をスローする

2.1.2戻り値

2.1.3待機のブロック

2.1.4タイムアウト待機

2.2同期キュー

3 AQS


java.utilパッケージのほとんどのコンテナは、スレッドセーフではありません。

コンテナーを複数のスレッドで使用する場合は、java.util.Collections:synchronizedXXXが提供するラッパー関数を使用して、通常のコンテナーをスレッドセーフな同期コンテナーに変えることができます。ただし、この方法では、コンテナの同期を使用するだけであり、非常に非効率的です。

java.util.concurrentパッケージは、効率的な並行コンテナーを提供します。通常のコンテナーとのインターフェースの一貫性を維持するために、utilパッケージのインターフェースが引き続き使用されます。これは、使いやすく、理解しやすいものです。

1コレクション

通常、コレクションにはリスト、セット、マップが含まれていると言われます。

List、Set、およびMapはすべて、java.utilパッケージによって提供されるインターフェースです。

1.1リスト

Listインターフェイスのすべての実装クラス:

より一般的に使用されるものには、次のものがあります。

  • 配列リスト
  • LinkedList

ArrayListコンテナはサイズ変更可能な配列であり、nullを含むすべての要素を許可します。

LinkedListコンテナは二重にリンクされたリストであり、nullを含むすべての要素も許可します。

ArrayListとLinkedListはどちらもスレッドセーフではありません。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //List<String> list = new LinkedList<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add("element"); //向list中添加元素"element"
                System.out.println(list); //打印list,存在遍历集合的行为
            }).start();
        }
    }
}

ArrayListまたはLinkedListのどちらを使用する場合でも、上記のコードを実行するとエラーが発生します。

スレッド「Thread-XX」の例外java.util.ConcurrentModificationException

この例外の理由は、スレッドは通常、他のスレッドがトラバースしているコレクションを変更することを許可されていないためです。この場合、反復の結果は未定義です。

ArrayListとLinkedListのスレッドのセキュリティが不安定な場合、java.utilパッケージとjava.util.concurrentパッケージがそれぞれ解決策を提供します。

java.utilパッケージによって提供されるソリューションは次のとおりです。

Collenctionsクラスを提供します。これは、コンテナーオブジェクトをスレッドセーフにすることができる一連のパッケージ化メソッドを提供します。[デコレータモード]

public static <T> Collection<T> synchronizedCollection(Collection<T> c)
public static <T> List<T> synchronizedList(List<T> list)
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
public static <T> Set<T> synchronizedSet(Set<T> s)
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s)

同期されていることを確認すると、これらのパッケージ化方法が同期ロックを使用してスレッドの同期を実現していることがわかります。

コードデモ:

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());
        //List<String> list = Collections.synchronizedList(new LinkedList<>());
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
                System.out.println(list);
            }).start();
        }
    }
}

コードを実行すると、java.util.ConcurrentModificationExceptionが報告されなくなります。

java.util.concurrentパッケージによって提供されるソリューションは次のとおりです。

CopyOnWriteArrayListクラスが提供され、ArrayListインスタンスの代わりにCopyOnWriteArrayListクラスのインスタンスが作成されます。

CopyOnWriteArrayListの原則:

スレッドがCopyOnWriteArrayListコンテナーにデータを書き込むとき、最初にコピーコンテナーをコピーアウトし、次にこのコピーコンテナーにデータを書き込み、最後にコピーコンテナーの参照アドレスを元のコンテナーアドレスに割り当てます。書き込みプロセス全体を通じて、他のスレッドがデータを読み取りたい場合でも、元のコンテナー内のデータを読み取ります。

CopyOnWriteLinkedListがないのはなぜですか?

その理由は、コピーオンライトのLinkedListには、従来のLinkedListと比較してパフォーマンス上の利点がないためです。

CopyOnWriteLinkedListの実装は、それを使用する時間と時間の無駄です。

コードデモ:

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Test {
    public static void main(String[] args) {
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
                System.out.println(list);
            }).start();
        }
    }
}

異常動作はありません。

1.2セット

Setインターフェイスのすべての実装クラス:

より一般的に使用されるものには、次のものがあります。

  • HashSet
  • TreeSet

HashSetコンテナーの最下層は、HashMapコンテナーのキーのコレクションであり、その要素は順序付けられておらず、繰り返すこともできません。nullは許可されますが、許可されるのは1つだけです。

TreeSetコンテナの最下層は、TreeMapコンテナのキーのコレクションです。その要素は順序付けられており、繰り返すことはできません。nullは許可されますが、許可されるのは1つだけです。

HashSetとTreeSetはどちらもスレッドセーフではありません。同様に、java.util.Collectionsクラスはパッケージ化メソッドを提供し、java.util.concurrentパッケージはスレッドセーフの問題を解決するためにCopyOnWriteArraySetクラスを提供します。

import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

public class Test {
    public static void main(String[] args) {
        Set<String> set = new HashSet<>(); //HashSet是线程不安全的
        //Set<String> set = new TreeSet<>(); //TreeSet是线程不安全的
        //解决方案1:java.util包提供了Collections类中的包装方法
        //Set<String> set = Collections.synchronizedSet(new HashSet<>());
        //Set<String> set = Collections.synchronizedSet(new TreeSet<>());
        //解决方案2:java.util.concurrent包提供了CopyOnWriteArraySet类
        //Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                set.add(Thread.currentThread().getName());
                System.out.println(set);
            }).start();
        }
    }
}

1.3マップ

Mapインターフェースのすべての実装クラス:

より一般的に使用されるものには、次のものがあります。

  • HashMap
  • TreeMap

HashMapコンテナは、配列+リンクリスト構造です。その要素は、キーと値のキーと値のペアです。キーは順序付けられておらず、繰り返し不可です。nullを指定できますが、1つだけです。値は繰り返し可能で、nullを使用できます。

TreeMapコンテナは赤黒木構造であり、その要素もキーと値のキーと値のペアです。キーは順序付けられており、繰り返し不可です。nullは許可されますが、1つだけです。値は繰り返し可能で、nullは許可されます。

HashMapとTreeMapはどちらもスレッドセーフではありません。同様に、java.util.Collectionsクラスはパッケージ化メソッドを提供し、java.util.concurrentパッケージはスレッドセーフの問題を解決するためのConcurrentHashMapクラスを提供します。

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class Test {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>(); //HashMap是线程不安全的
        //Map<String, String> map = new TreeMap<>(); //TreeMap是线程不安全的
        //解决方案1:java.util包提供了Collections类中的包装方法
        //Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
        //Map<String, String> map = Collections.synchronizedMap(new TreeMap<>());
        //解决方案2:java.util.concurrent包提供了ConcurrentHashMap类
        //Map<String, String> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 50; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(), Thread.currentThread().getName());
                System.out.println(map);
            }).start();
        }
    }
}

 

2ブロッキングキュー

キューは一種の線形テーブルです。これは、テーブルのバックエンドからの挿入と、テーブルのフロントエンドからの値(削除)のみを許可します。つまり、FIFO(先入れ先出し)、先入れ先出し。

java.util.concurrentパッケージでは、ブロッキングキューが同期コンテナとして使用され、マルチスレッドの効率的で安全なデータ転送の問題を解決します。

どのような状況でキューがブロックされますか?

  • キューがいっぱいでブロックされています
  • キューは空で、ブロックされており、本番環境を待機しています

ブロッキングキュー:BlockingQueue

BlockingQueueは、java.util.concurrentパッケージのインターフェースであり、Collectionインターフェースも実装します。

BlockingQueueインターフェースのすべての実装クラス:

より一般的に使用されるものには、次のものがあります。

  • ArrayBlockingQueue、配列ベースのブロッキングキュー
  • LinkedBlockingQueue、リンクリストに基づくブロッキングキュー
  • SynchronousQueue、同期キュー

2.1ブロッキングキューを操作するための4セットのAPI

java.util.concurrentパッケージは、ブロッキングキューを操作するための4セットのAPIを提供します。

  • 例外をスローする
  • 戻り値
  • 待機のブロック
  • タイムアウト待機

それらには独自の目的があり、柔軟に使用できます。

操作方法 例外をスローする 戻り値 待機のブロック タイムアウト待機
追加 追加 提供 置く 提供(、、)
削除する 削除する 投票 取る poll(、)
キューの先頭を決定する 素子 ピーク - -

2.1.1例外をスローする

  • add()、ブロッキングキューの最後に要素を挿入すると、操作は成功してtrueを返します。キューがいっぱいでブロッキングが発生した場合、例外java.lang.IllegalStateExceptionが報告されます。
  • remove()、ブロッキングキューの先頭から値を取得(削除)、操作は成功し、値を返します。キューが空の場合、例外java.util.NoSuchElementExceptionが報告されます。
  • element()、ブロッキングキューの最初の要素を表示します。操作はキューの最初の要素の値を正常に返します。キューが空の場合、例外java.util.NoSuchElementExceptionが報告されます。

テストコード:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    
    public static void test1() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        System.out.println(blockingQueue.add("a"));
        System.out.println(blockingQueue.add("b"));
        //System.out.println(blockingQueue.add("c")); //队列已满,发生阻塞,发生异常
        System.out.println(blockingQueue.element());
        System.out.println(blockingQueue.remove());
        System.out.println(blockingQueue.remove());
        //System.out.println(blockingQueue.remove()); //队列为空,发生异常
        //System.out.println(blockingQueue.element()); //队列为空,发生异常
    }
}

演算結果:

true
true
a
a
b

2.1.2戻り値

  • オファー()、ブロックキューの最後に要素を挿入し、操作が成功した場合はtrueを返し、キューがいっぱいでブロックされている場合はfalseを返します。
  • poll()、ブロッキングキューの先頭から値を取得(削除)し、操作が成功した場合は値を返し、キューが空の場合はnullを返します。
  • peek()は、ブロッキングキューの最初の要素を確認し、操作が成功した場合はキューの最初の要素の値を返し、キューが空の場合はnullを返します。

テストコード:

​import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test2();
    }

    public static void test2() {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        System.out.println(blockingQueue.offer("a"));
        System.out.println(blockingQueue.offer("b"));
        System.out.println(blockingQueue.offer("c"));
        System.out.println(blockingQueue.peek());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.poll());
        System.out.println(blockingQueue.peek());
    }
}

演算結果:

true
true
false
a
a
b
null
null

2.1.3待機のブロック

  • put()、ブロックキューの最後に要素を挿入します。戻り値はありません。キューがいっぱいでブロッキングが発生した場合、現在のスレッドは待機します。
  • take()、ブロッキングキューの先頭から値を取得(削除)すると、操作は成功し、値を返します。キューが空の場合、現在のスレッドは待機します。

テストコード:

​import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test3();
    }

    public static void test3() throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        blockingQueue.put("a");
        blockingQueue.put("b");
        System.out.println("point1");
        //blockingQueue.put("c"); //队列已满,发生阻塞,线程等待
        System.out.println("point2");
        System.out.println(blockingQueue.take());
        System.out.println(blockingQueue.take());
        System.out.println("point3");
        //System.out.println(blockingQueue.take()); //队列为空,线程等待
        System.out.println("point4");
    }
}

演算結果:

POINT1
ポイント2
A
B
POINT3
POINT4

2.1.4タイムアウト待機

  • オファー(、、):ブロックキューの最後に要素を挿入すると、操作が成功すると操作はtrueを返します。キューがいっぱいでブロックされている場合、現在のスレッドは待機します。一定時間待機した後、 falseを返し、下向きに実行し続けます。
  • poll(、):ブロッキングキューの先頭から値を取得(削除)すると、操作は正常に値を返します。キューが空の場合、現在のスレッドは待機します。一定時間待機した後、nullを返し、続行します。下向きに実行します。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        test4();
    }

    public static void test4() throws InterruptedException {
        BlockingQueue blockingQueue = new ArrayBlockingQueue(2); //设置阻塞队列的大小为2
        System.out.println(blockingQueue.offer("a", 2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("b", 2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.offer("c", 2, TimeUnit.SECONDS)); //超时等待时间:2秒
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS));
        System.out.println(blockingQueue.poll(2, TimeUnit.SECONDS)); //超时等待时间:2秒
        System.out.println("ok");
    }
}

演算結果:

true
true
false
a
b
null
ok

2.2同期キュー

同期キュー:SynchronousQueue

SynchronousQueueクラスは、BlockingQueueインターフェイスの実装クラスです。

SynchronousQueueコンテナの特徴:容量は1です。要素を入れた後、要素を中に入れる前に、要素が取り出されるのを待つ必要があります。

テストコード:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;

public class Test {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName() + " put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName() + " put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " take " + blockingQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2").start();
    }
}

演算結果:

T1プット
1T2テイク
1T1プット
2T2テイク
2T1プット
3T2テイク3

 

3 AQS

AQS:AbstractQueuedSynchronizer、抽象キューシンクロナイザー。

AQSは、タイプvolatile intの可変状態と、二重リンクリストによって実装されたキューを維持します。キュー内の要素はスレッド(スレッドオブジェクト)です。

状態の初期値は0です。

スレッドA(ReentrantLockオブジェクトを使用)がlock()メソッドを呼び出すと、tryAcquire()メソッドを呼び出して、ロックリソース[CAS]の取得を試みます。

状態が0の場合、スレッドAはロックリソースstate + 1を正常に取得し、スレッドAは排他スレッドとして設定されます。

ロックリソースがすでに別のスレッドBによって独占されている場合、つまり状態が0でない場合、スレッドAはロックリソースの取得に失敗し、スピンして待機します。スレッドAは特定の回数スピンし、CLHキューに入ります。 。

スレッドBがロックリソースを保持しているときにlock()メソッドを再度呼び出すと、ロックの再入が発生します(state + 1)。スレッドBがunlock()メソッドを呼び出すたびに(state-1)、状態が0になるまでスレッドBは解放します。リソースをロックします。

スレッドBがロックリソースを解放した後、CLHキュー内のスレッドは引き続きロックリソースを取得します。

tryAcquire()ソースコード:

 

ビデオリンクの学習:

https://www.bilibili.com/video/BV1B7411L7tE

加油!(d•_•)d

おすすめ

転載: blog.csdn.net/qq_42082161/article/details/114002333