Java並行プログラミング (5) スレッド同期 [CAS|アトミッククラス|同期コンテナクラス|同期ツールクラス]

CAS

概要

CAS の正式名称は Compare-And-Swap です。これは CPU のアトミック命令であり、共有データに対する同時操作をハードウェアでサポートします。その機能は、CPUが特定の瞬間に2つの値が等しいかどうかを比較することです

基本原理: CAS は動作中に、まずメイン メモリの値とスレッドの作業メモリの値が等しいかどうかを比較し、等しい場合、メイン メモリの値を新しい値に更新します。等しくない場合、交換されません(等しくない場合は、常にスピンを通じて値を更新しようとします)

CAS 命令には次の問題があります。

  • ABA 問題: 2 つの瞬間の値を比較すると、ABA 問題が発生します。最初は A でしたが、途中で B に変更され、その後 A に戻りました。CAS 検出では値が変化していないと考えられますが、それは変わったという事実。
    • 解決策: JDK1.5 の AtomicStampedReference を使用して、ABA 問題を解決できます。基本的な考え方は、バージョン番号を増やし、変更された現在の値と期限切れの値が一致しているかどうかを確認することです。AtomicStampedReference は、現在の参照と期限切れの参照が等しいかどうかに基づいて CAS 操作を実行します。
  • 長いサイクル時間と高いオーバーヘッド: CAS が失敗した場合、常にスピンして試行します。CAS が長期間失敗すると、CPU に多大なオーバーヘッドが発生する可能性があります。したがって、競合が頻繁に発生するシナリオをCAS処理するためにプリミティブを使用することはお勧めできません (CASこれはオプティミスティック ロック メカニズムです。オプティミスティック ロックは、競合が頻繁に発生するシナリオには適していません。この場合は、ペシミスティック ロック メカニズムを選択できます)。

CASアルゴリズムの例

package com.bierce;

public class TestCAS {
    public static void main(String[] args) {
        final CAS cas = new CAS();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                int expectedValue = cas.getValue();//每次更新前必须获取内存中最新的值,因为可能与compareAndSwap中第一次获取的Vaule不一样
                boolean updateRes = cas.compareAndSet(expectedValue, (int) Math.random() * 101);
                System.out.print(updateRes + " "); // true true true true true true true true true true
            }).start();
        }
    }
}
class CAS{
    private int value;
    //获取内存值
    public synchronized  int getValue(){
        return value;
    }
    //比较内存值
    public synchronized  int compareAndSwap(int expectedValue, int newValue){
        int oldValue = value;//
        if (oldValue == expectedValue){
            this.value = newValue;
        }
        return oldValue;
    }
    //设置内存值
    public synchronized  boolean compareAndSet(int expectedValue, int newValue){
        return expectedValue == compareAndSwap(expectedValue, newValue);
    }
}

アトミッククラス

概要

マルチスレッド環境では、ロック ( synchronized/Lock ) によってデータ操作の一貫性を確保できますが、効率が非常に面倒です。したがって、Java は、java.util.concurrent.atomic使いやすく、高性能かつ効率的なアトミック操作用のカプセル化されたクラスであるパッケージを提供します。Atomic クラス メソッドにはロックがありません。基礎となるコアは、Unsafe クラスの volatile および CAS アルゴリズム操作を通じてスレッドの可視性を確保し、データ操作のアトミック性を確保することです。

分類

java.util.concurrent.atomic パッケージのアトミック クラス

AtomicInteger の練習

package com.bierce;
import java.util.concurrent.atomic.AtomicInteger;
public class TestAtomicDemo {
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();

        //模拟多线程,通过原子类保证变量的原子性
        for(int i =0; i < 10; i++){
            new Thread(atomicDemo).start();
        }
    }
}
class AtomicDemo implements Runnable{
    //AtomicInteger原子类提供很多API,根据需求使用
    private AtomicInteger sn = new AtomicInteger(); 
    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //getAndIncrement() :类似于 i++
        //incrementAndGet() :类似于 ++i
        //System.out.print(sn.getAndIncrement() + " "); // 输出:1 9 8 7 0 5 2 6 4 3
        System.out.print(sn.incrementAndGet() + " "); // 输出:1 10 9 5 2 8 3 6 7 4
    }
}

同期コンテナクラス

  • ConcurrentHashMap : 同期された HashMap の同時実装
  • ConcurrentSkipListMap: 同期された TreeMap の同時実装
  • CopyOnWriteArrayList:同時読み取り、またはコレクション データの走査要件がリスト更新要件よりはるかに大きい場合に適しています。
  • ThreadLocal: スペースを時間と交換するというアイデアを使用し、スレッドごとにコピーをコピーし、スレッドを分離することでスレッドの安全性を実現します。
  • ブロッキングキュー
  • ConcurrentLinkedQueue: CAS を通じてスレッドの安全性を確保し、遅延更新戦略を採用し、スループットを向上させます。

同時ハッシュマップ

ConcurrentHashMap は内部的に「ロック セグメンテーション」のアイデアを採用して HashTable の排他ロックを置き換え、同時実行性を制御するための同期操作を追加し、パフォーマンスを向上させますが、基礎となる実装は JDK1.7 と JDK1.8 バージョンで異なります。

  • JDK1.7版:最下層はReentrantLock+Segment+HashEntry、JDK1.8版:最下層はsynchronized+CAS+HashEntry+赤黒ツリー
  • JDK1.8 ではロック粒度が減少しています。JDK1.7 バージョンのロック粒度はセグメントに基づいており、複数の HashEntry が含まれていますが、JDK1.8 のロック粒度は HashEntry (最初のノード) です。
  • JDK1.8 のデータ構造はより単純です。JDK1.8 は同期に synchronized を使用するため、Segment のようなデータ構造は必要ありません。
  • JDK1.8 は、赤黒ツリーを使用してリンク リストを最適化します。長いリンク リストに基づく走査は非常に長いプロセスですが、赤黒ツリーの走査効率は非常に高速です。
  • 拡張機能: reentrantLock の代わりに synchronized を使用する理由
    • 粒度が低下するため、synchronized は ReentrantLock よりも悪くありません。粗粒度のロックでは、ReentrantLock は Condition を使用して各低粒度の境界を制御でき、より柔軟になりますが、低粒度では Condition の利点が失われます。
    • JVM 開発チームは同期を諦めたことはなく、JVM ベースの同期最適化スペースはさらに大きくなり、埋め込みキーワードを使用する方が API を使用するより自然です。
    • 大量のデータ操作が行われる場合、API ベースの ReentrantLock は、JVM メモリの負荷により、より多くのメモリを消費します。

CopyOnWriteArrayList の練習

package com.bierce;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * CopyOnWriteArrayList : 支持并发读取时写入
 * 注意:添加数据操作多时,效率比较低,因为每次添加都会进行复制一个新的列表,开销大;
 *      而并发迭代操作多时效率高
 */
public class TestCopyOnWriteArrayList {
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayListDemo copyOnWriteArrayListDemo = new CopyOnWriteArrayListDemo();

        for (int i = 0; i < 10; i++) {
            new Thread(copyOnWriteArrayListDemo).start();
        }
    }
}
class CopyOnWriteArrayListDemo implements  Runnable{
    // 不支持并发修改场景,Exception in thread "Thread-1" java.util.ConcurrentModificationException
    //private static List<String> list = Collections.synchronizedList(new ArrayList<String>());
    public static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    static{
        list.add("贾宝玉");
        list.add("林黛玉");
        list.add("薛宝钗");
    }
    @Override
    public void run() {
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next());
            list.add("test");
        }
    }
}

同期ツール

  • カウントダウンラッチ
  • サイクリックバリア
  • リソースアクセス制御セマフォ

カウントダウンラッチ(ラッチ)

概要

  • CountDownLatch は、複数のスレッド間の同期を調整し、スレッド間の通信を実装するために使用される同期ツール クラスです。
  • CountDownLatch は、あるスレッドが実行を続行する前に他のスレッドが作業を完了するのを待つシナリオに適しています。

原理

CountDownLatch の最下層はカウンターを通じて実装されており、カウンターの初期値はスレッドの数です。スレッドがタスクを完了するたびに、それに応じてカウンタ値が 1 ずつ減らされます。カウンタが 0 に達すると、すべてのスレッドがタスクを完了したことを意味し、ロックで待機しているスレッドはタスクの実行を続行できます。

練習する

package com.bierce;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.CountDownLatch;

public class TestCountDownLatchDemo {
    public static void main(String[] args) {
        final CountDownLatch countDownLatch = new CountDownLatch(5);//声明5个线程
        CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo(countDownLatch);

        //long start = System.currentTimeMillis();
        Instant start = Instant.now();//JDK8新特性API
        for (int i = 0; i < 5; i++) { //此处循环次数必须和countDownLatch创建的线程个数一致
            new Thread(countDownLatchDemo).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
        }
        //long end = System.currentTimeMillis();
        Instant end = Instant.now();
        //System.out.println("执行完成耗费时长: " + (end - start));
        System.out.println("执行完成耗费时长: " + Duration.between(end, start).toMillis()); // 110ms
    }
}
class CountDownLatchDemo implements Runnable{
    private CountDownLatch countDownLatch;
    public CountDownLatchDemo(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }
    @Override
    public void run() {
        synchronized (this){ //加锁保证并发安全
            try {
                for (int i = 0; i < 10000; i++) {
                    if (i % 2 == 0) {
                        System.out.println(i);
                    }
                }
            }finally {
                countDownLatch.countDown(); //线程完成后执行减一
            }
        }
    }
}

おすすめ

転載: blog.csdn.net/qq_34020761/article/details/132245992