【並行プログラミング8部構成】プロセス、スレッド、並行プログラミングの3大特徴

目次

プロセスとスレッドの概念は何ですか?

プロセスとは、実行中のプログラムを指します。たとえば、DingTalk またはブラウザを使用する場合、このプログラムを起動する必要があり、オペレーティング システムはこのプログラムに特定のリソースを割り当てます (メモリ リソースを占有します)。スレッドは CPU スケジューリングの基本単位であり、各スレッドは特定のプロセスのコードの特定のフラグメントを実行します。

シリアル、パラレル、同時実行の概念は?

シリアル化とは、1 つずつキューに並べることを意味し、最初のものが終了した後は 2 つ目のみが参加できます。
並列処理とは、同時に処理することを意味します。
ここでの同時実行性は、3 つの高校の同時実行性の高さの問題ではなく、マルチスレッドにおける同時実行性の概念 (CPU スケジューリング スレッドの概念) です。CPUは非常に短い時間内に切り替えを繰り返し、異なるスレッドを実行するため、並列的に見えますが、CPUが高速に切り替えているだけです。

並列処理には同時実行性が含まれます。
並列処理とは、マルチコア CPU が複数のスレッドを同時にスケジュールすることを意味します。これは、複数のスレッドが同時に実行されることを意味します。
シングルコア CPU は並列効果を実現できませんが、シングルコア CPU は同時実行性を備えています。

同期、非同期、ブロッキング、ノンブロッキングの概念は何ですか?

同期と非同期: 特定の関数を実行した後、呼び出し先が積極的に情報をフィードバックするかどうか。
ブロッキングとノンブロッキング: 関数の実行後、呼び出し元は結果に関するフィードバックを待つ必要がありますか?

2 つの概念は似ているように見えるかもしれませんが、焦点はまったく異なります。
同期ブロック: たとえば、鍋を使って水を沸騰させた場合、水が沸騰しても通知されません。お湯の沸騰が始まったら、水が沸騰するまで待つ必要があります。

同期ノンブロッキング: たとえば、鍋を使って水を沸騰させた場合、水が沸騰しても通知されません。お湯の沸騰を開始した後は、沸騰を待つ必要はなく、他の機能を実行できますが、時々お湯が沸騰しているかどうかを確認する必要があります。

非同期ブロック: たとえば、やかんで水を沸騰させると、水が沸騰した後に水が沸騰したことが通知されます。お湯の沸騰が始まったら、水が沸騰するまで待つ必要があります。

非同期ノンブロッキング:たとえば、やかんで水を沸騰させると、水が沸騰した後に水が沸騰したことが通知されます。湯沸かし開始後は沸騰を待つ必要がなく、他の機能を利用することができます。

非同期ノンブロッキングが最も効果的 通常の開発中、効率を向上させる最善の方法は、非同期ノンブロッキングを使用して一部のマルチスレッド タスクを処理することです。

スレッドはどのように作成されますか?

Thread クラスを継承し、run メソッドをオーバーライドします。

スレッドを開始するには、start メソッドを呼び出します。これにより、新しいスレッドが作成され、スレッドのタスクが実行されます。run メソッドを直接呼び出すと、現在のスレッドで run メソッドのビジネス ロジックが実行されます。

class Thread implements Runnable {
    
    }
public class MiTest {
    
    
    public static void main(String[] args) {
    
    
        MyJob t1 = new MyJob();
        t1.start();
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}
class MyJob extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("MyJob:" + i);
        }
    }
}
Runnable インターフェイスを実装し、run メソッドをオーバーライドする
public class MiTest {
    
    
    public static void main(String[] args) {
    
    
        MyRunnable myRunnable = new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        t1.start();
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("main:" + i);
        }
    }
}
class MyRunnable implements Runnable{
    
    
    @Override
    public void run() {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("MyRunnable:" + i);
        }
    }
}
Callableを実装しcallメソッドを書き換えてFutureTaskと連携する

Callable は通常、結果を返す非ブロッキング実行メソッドに使用されます。同期かつノンブロッキング。

public class FutureTask<V> implements RunnableFuture<V> {
    
    }

public interface RunnableFuture<V> extends Runnable, Future<V> {
    
    }
public class MiTest {
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        //1. 创建MyCallable
        MyCallable myCallable = new MyCallable();
        //2. 创建FutureTask,传入Callable
        FutureTask futureTask = new FutureTask(myCallable);
        //3. 创建Thread线程
        Thread t1 = new Thread(futureTask);
        //4. 启动线程
        t1.start();
        //5. 做一些操作
        //6. 要结果
        Object count = futureTask.get();
        System.out.println("总和为:" + count);
    }
}
class MyCallable implements Callable{
    
    
    @Override
    public Object call() throws Exception {
    
    
        int count = 0;
        for (int i = 0; i < 100; i++) {
    
    
            count += i;
        }
        return count;
    }
}
スレッドプールに基づいてスレッドを構築する

タスクはスレッド プールに送信され、スレッド プールはタスクを実行するワーカー スレッドを作成します。

public class ThreadPoolExecutor extends AbstractExecutorService {
    
    
...
private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
    
    
    ...
}
匿名の内部クラスとラムダ式のアプローチ
// 匿名内部类方式:
  Thread t1 = new Thread(new Runnable() {
    
    
      @Override
      public void run() {
    
    
          for (int i = 0; i < 1000; i++) {
    
    
              System.out.println("匿名内部类:" + i);
          }
      }
  });
  
// lambda方式:
  Thread t2 = new Thread(() -> {
    
    
      for (int i = 0; i < 100; i++) {
    
    
          System.out.println("lambda:" + i);
      }
  });
要約: 最下層を追求する唯一の方法は、Runnble を実装することです。

スレッドの状態とは何ですか? Java のスレッドの状態は何ですか?

オペレーティング システム レベル: 5 つの状態 (通常は従来のスレッドの状態)
ここに画像の説明を挿入します
Java の 6 つの状態
ここに画像の説明を挿入します

ここに画像の説明を挿入します

  • NEW : Thread オブジェクトは作成されますが、start メソッドはまだ実行されていません。

  • RUNNABLE : Thread オブジェクトが start メソッドを呼び出すとき、そのオブジェクトは RUNNABLE 状態 (CPU スケジューリング/スケジューリングなし) になります。

  • BLOCKED : synchronized は同期ロックを取得せず、ブロックされています。

  • WAITING : wait メソッドを呼び出すと WAITING 状態になるため、手動でウェイクアップする必要があります。

  • TIME_WAITING : sleep メソッドまたは join メソッドを呼び出すと自動的にウェイクアップします。手動でウェイクアップする必要はありません。

  • TERMINATED : run メソッドが実行され、スレッドのライフサイクルが終了します。

BLOCKEDWAITINGTIME_WAITING : これら 3 つの状態では、CPU は現在のスレッドをスケジュールしないため、すべてブロック状態と待機状態として理解できます。

Java の 6 つの状態の例

//NEW:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
    });
    System.out.println(t1.getState());
}


//RUNNABLE:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(true){
    
    
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//BLOCKED:
public static void main(String[] args) throws InterruptedException {
    
    
    Object obj = new Object();
    Thread t1 = new Thread(() -> {
    
    
        // t1线程拿不到锁资源,导致变为BLOCKED状态
        synchronized (obj){
    
    
        }
    });
    // main线程拿到obj的锁资源
    synchronized (obj) {
    
    
        t1.start();
        Thread.sleep(500);
        System.out.println(t1.getState());
    }
}


//WAITING:
public static void main(String[] args) throws InterruptedException {
    
    
    Object obj = new Object();
    Thread t1 = new Thread(() -> {
    
    
        synchronized (obj){
    
    
            try {
    
    
                obj.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//TIMED_WAITING:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(500);
    System.out.println(t1.getState());
}


//TERMINATED:
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(500);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(1000);
    System.out.println(t1.getState());
}

スレッドの一般的なメソッドと例は何ですか?

現在のスレッドを取得する

スレッドの静的メソッドは現在のスレッド オブジェクトを取得します

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
	// 获取当前线程的方法
    Thread main = Thread.currentThread();
    System.out.println(main);
    // "Thread[" + getName() + "," + getPriority() + "," +  group.getName() + "]";
    // Thread[main,5,main]
}
スレッドの名前を設定します

Thread オブジェクトを構築した後は、後でエラーのトラブルシューティングを行うために、必ず意味のある名前を設定してください。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        System.out.println(Thread.currentThread().getName());
    });
    t1.setName("模块-功能-计数器");
    t1.start();
}
スレッドの優先順位を設定する

実は、これは CPU スケジューリング スレッドの優先度であり、Java のスレッドには 10 レベルの優先度が設定されており、1 から 10 までの任意の整数が取られます。この範囲を超えた場合、パラメータ例外エラーは解消されます。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("t1:" + i);
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            System.out.println("t2:" + i);
        }
    });
    t1.setPriority(1);
    t2.setPriority(10);
    t2.start();
    t1.start();
}
スレッドの譲歩を設定する

Thread の静的メソッド yield を使用すると、現在のスレッドを実行状態から準備完了状態に変更できます。

public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            if(i == 50){
    
    
                Thread.yield();
            }
            System.out.println("t1:" + i);
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            System.out.println("t2:" + i);
        }
    });
    t2.start();
    t1.start();
}
スレッドのスリープを設定する

Thread の静的メソッド sleep を使用すると、スレッドを実行状態から待機状態に変更できます。sleep には 2 つのメソッド オーバーロードがあります。

  • 最初のものはネイティブによって変更されており、スレッドを待機状態に変える効果があります。
  • 2 つ目は、ミリ秒とナノ秒で渡すことができるメソッドです (ナノ秒の値が 0.5 ミリ秒以上の場合は、休止中のミリ秒の値に 1 を加算します。渡されたミリ秒の値が 0 で、ナノ秒の値が 0 でない場合) 、その後 1 ミリ秒スリープします)
// sleep 会抛出一个 InterruptedException
public static void main(String[] args) throws InterruptedException {
    
    
    System.out.println(System.currentTimeMillis());
    Thread.sleep(1000);
    System.out.println(System.currentTimeMillis());
}
スレッドのプリエンプションを設定する

スレッドの非静的メソッド join メソッドは、特定のスレッドで呼び出す必要があります。

t1.join() がメイン スレッドで呼び出された場合、メイン スレッドは待機状態になり、すべての t1 スレッドが実行を完了するまで待機してから、準備完了状態に戻って CPU スケジューリングを待機する必要があります。

t1.join(2000) がメイン スレッドで呼び出された場合、メイン スレッドは待機状態になり、t1 が実行されるまで 2 秒間待機してから、準備完了状態に戻って CPU スケジューリングを待機する必要があります。待機期間中に t1 が終了した場合、メインスレッドは自動的に準備完了となり、CPU スケジューリングを待機します。

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println("t1:" + i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.start();
    for (int i = 0; i < 10; i++) {
    
    
        System.out.println("main:" + i);
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        if (i == 1){
    
    
            try {
    
    
                t1.join(2000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}
デーモンスレッドをセットアップする

デフォルトでは、スレッドはすべて非デーモン スレッドであり、プログラム内に非デーモン スレッドがなくなると、JVM は現在の JVM を終了します。
メイン スレッドはデフォルトで非デーモン スレッドです。メイン スレッドの実行が終了した場合は、現在の JVM に非デーモン スレッドが存在するかどうかを確認する必要があります。JVM がない場合は、JVM を直接停止します。

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println("t1:" + i);
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    });
    t1.setDaemon(true);
    t1.start();
}
スレッドの待機とウェイクアップを設定する

同期ロックリソースを取得したスレッドは、 wait メソッドを通じてロック待機プールに入ることができ、ロックリソースは解放されます。
同期ロック リソースを取得したスレッドは、待機プール内のスレッドをウェイクアップし、 notify または NoticeAll メソッドを通じてロックプールに追加できます。

通知は、待機プール内のスレッドをロック プールにランダムにウェイクアップします。
NoticeAll は、待機プール内のすべてのスレッドをウェイクアップし、それらをロック プールに追加します。

wait メソッド、notify メソッド、および NorifyAll メソッドを呼び出す場合は、特定のオブジェクトのロックに基づく情報保守を操作する必要があるため、同期された変更コード ブロックまたはメソッド内で実行する必要があります。

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        sync();
    },"t1");

    Thread t2 = new Thread(() -> {
    
    
        sync();
    },"t2");
    t1.start();
    t2.start();
    Thread.sleep(12000);
    synchronized (MiTest.class) {
    
    
        MiTest.class.notifyAll();
    }
}

public static synchronized void sync()  {
    
    
    try {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            if(i == 5) {
    
    
                MiTest.class.wait();
            }
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName());
        }
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
}

スレッドを終了するにはどのような方法がありますか?

スレッドを終了するにはさまざまな方法がありますが、最も一般的に使用される方法は、リターンまたは例外のスローによってスレッドが終了するかどうかに関係なく、スレッドの run メソッドを終了することです。

stopメソッド(未使用)

何をしていてもスレッドを強制的に終了することはお勧めできません

    @Deprecated
    public final void stop() {
    
    ...}
public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
    t1.start();
    Thread.sleep(500);
    t1.stop();
    System.out.println(t1.getState());
}
共有変数を使用する (めったに使用されない)

この方法はあまり使用されず、一部のスレッドが無限ループを実行し続ける可能性があります。
共有変数を変更することで無限ループを中断し、スレッドをループから抜け出させて、run メソッドを終了することができます。

static volatile boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(flag){
    
    
            // 处理任务
        }
        System.out.println("任务结束");
    });
    t1.start();
    Thread.sleep(500);
    flag = false;
}
割り込みモード

デフォルトでは、スレッド割り込みフラグ ビット内に割り込みフラグ ビットがあります: false

シェア変数モード

public static void main(String[] args) throws InterruptedException {
    
    
    // 线程默认情况下,    interrupt标记位:false
    System.out.println(Thread.currentThread().isInterrupted());
    // 执行interrupt之后,再次查看打断信息
    Thread.currentThread().interrupt();
    // interrupt标记位:ture
    System.out.println(Thread.currentThread().isInterrupted());
    // 返回当前线程,并归位为 false,interrupt标记位:ture
    System.out.println(Thread.interrupted());
    // 已经归位了
    System.out.println(Thread.interrupted());

    // =====================================================
    Thread t1 = new Thread(() -> {
    
    
        while(!Thread.currentThread().isInterrupted()){
    
    
            // 处理业务
        }
        System.out.println("t1结束");
    });
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

WAITING または TIMED_WAITING 状態のスレッドを中断し、例外をスローして自分で処理する
このスレッドを停止する方法は、最も一般的に使用される方法であり、フレームワークや JUC でも最も一般的です。

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while(true){
    
    
            // 获取任务
            // 拿到任务,执行任务
            // 没有任务了,让线程休眠
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
                System.out.println("基于打断形式结束当前线程");
                return;
            }
        }
    });
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
}

待機とスリープの違いは何ですか?

  • sleep は Thread クラスの静的メソッドであり、wait は Object クラスのメソッドです。
  • sleep は TIMED_WAITING に属し、自動的に目覚めます。wait は WAITING に属し、手動で目覚める必要があります。
  • sleep メソッドはロックを保持したまま実行され、ロック リソースは解放されませんが、wait メソッドの実行後、ロック リソースは解放されます。
  • スリープはロックを保持した状態でも、ロックを保持していない状態でも実行できます。wait メソッドはロックがある場合にのみ実行する必要があります。

wait メソッドは、ロックを保持しているスレッドを _owner から _WaitSet コレクションにスローします。この操作は ObjectMonitor オブジェクトを変更しています。同期されたロックが保持されていない場合、ObjectMonitor オブジェクトは操作できません。

同時プログラミングの 3 つの主な特徴は何ですか?

  • 原子性
  • 可視性
  • 秩序

原子性

原子性の概念

JMM (Java メモリ モデル)。ハードウェアやオペレーティング システムが異なれば、メモリ操作には一定の違いがあります。異なるオペレーティング システム上の同じコードで発生するさまざまな問題を解決するために、Java は JMM を使用して、さまざまなハードウェアやオペレーティング システムによって生じる差異をシールドします。

Java の同時プログラミングをクロスプラットフォームにします。

JMMではすべての変数をメインメモリに格納することが規定されており、スレッド内で計算を行うためには動作時にメインメモリからスレッドメモリ(CPUメモリ)にコピーする必要があります。次に、それをメイン メモリに書き戻します (必ずしもそうである必要はありません)。

アトミック性の定義: アトミック性とは、操作が分割不可能で中断できないことを意味し、1 つのスレッドの実行中に、別のスレッドが影響を与えることはありません。

同時プログラミングの原子性をコードで示します。

private static int count;

public static void increment(){
    
    
    try {
    
    
        Thread.sleep(10);
    } catch (InterruptedException e) {
    
    
        e.printStackTrace();
    }
    count++;
}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
           increment();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

現在のプログラム: マルチスレッド操作でデータを共有すると、期待される結果が最終結果と一致しません。

原子性: 重要なリソースに対するマルチスレッド操作。期待される結果は最終結果と一致します。

このプログラムを分析すると、++ の演算は 3 つの部分に分かれていることがわかります。まず、スレッドがメイン メモリからデータを取得して CPU レジスタに保存し、その後、スレッドで +1 演算を実行します。レジスタに書き込み、最後に結果をメイン メモリに書き戻します。

同時プログラミングのアトミック性を確保する
同期した

++の演算は命令から見ることができるので

画像.png

synchronized キーワードをメソッドに追加するか、synchronized コード ブロックを使用してアトミック性を確保できます。

synchronized を使用すると、複数のスレッドが重要なリソースを同時に操作することを防ぐことができ、同時に 1 つのスレッドだけが重要なリソースを操作することになります。

画像.png

CAS

CASとは一体何なのでしょうか?

比較とスワップは比較とスワップであり、CPU 同時実行プリミティブです。

メモリ内の特定の位置の値を置き換える場合、まずメモリ内の値が期待値と一致するかどうかを確認し、一致する場合は置換操作を実行します。この操作はアトミック操作です。

Java の安全でないベースのクラスは、CAS を操作するためのメソッドを提供します。JVM は、そのメソッドを CAS アセンブリ命令に実装するのに役立ちます。

ただし、CAS は単なる比較と交換なので、元の値を取得する操作は自分で実装する必要があることに注意してください。

private static AtomicInteger count = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            count.incrementAndGet();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            count.incrementAndGet();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

Doug Lea は、現在見られる AtomicInteger や他の多くのアトミック クラスを含む、CAS に基づくいくつかのアトミック クラスの実装を支援してくれました。

CAS の欠点: CAS は変数の操作がアトミックであることのみを保証し、複数行のコードに対してアトミック性を実現することはできません。

CAS に関する質問:

  • ABA 問題: 問題は次のとおりです. ABA 問題を解決するには、バージョン番号を導入できます。Java は、クラスが CAS 内にある場合に、各バージョンにバージョン番号を追加する操作を提供します。AtomicStampeReference画像.png
  • AtomicStampedReference が CAS にある場合、元の値を判断するだけでなく、バ​​ージョン情報も比較します。
  • public static void main(String[] args) {
          
          
        AtomicStampedReference<String> reference = new AtomicStampedReference<>("AAA",1);
    
        String oldValue = reference.getReference();
        int oldVersion = reference.getStamp();
    
        boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
        System.out.println("修改1版本的:" + b);
    
        boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
        System.out.println("修改2版本的:" + c);
    }
    
  • スピン時間が長すぎる問題:
    • CAS が合計でループする回数を指定できます。この回数を超えると、直接失敗するか、スレッドがハングします。(スピンロック、アダプティブスピンロック)
    • CAS が一度失敗した後、この操作を一時的に保存し、後で結果を取得する必要がある場合、すべての一時的な操作を実行して最終結果を返すことができます。
ロックロック

Lock は、JDK1.5 で Doug Lea によって開発されました。そのパフォーマンスは、JDK1.5 で同期したものよりもはるかに優れています。ただし、JDK1.6 で最適化された同期後は、パフォーマンスに大きな違いはありませんが、処理が複雑な場合は、同時実行性を考慮すると、ReentrantLock が推奨され、パフォーマンスが向上します。

実現方法:

private static int count;

private static ReentrantLock lock = new ReentrantLock();

public static void increment()  {
    
    
    lock.lock();
    try {
    
    
        count++;
        try {
    
    
            Thread.sleep(10);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    } finally {
    
    
        lock.unlock();
    }


}

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    Thread t2 = new Thread(() -> {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            increment();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(count);
}

ReentrantLock は synchronized と直接比較でき、機能的にはどちらもロックです。

ただし、ReentrantLock には同期よりも豊富な機能があります。

ReentrantLock の最下層は AQS に基づいて実装されており、ロック操作を実装するために CAS に基づいて維持される状態変数があります。

スレッドローカル

Java の 4 つの参照型

Java で使用される参照型は、strong、soft、weak、および virtualです。

ユーザー user = 新しいユーザー();

Java で最も一般的なのは強参照であり、オブジェクトが参照変数に割り当てられる場合、その参照変数は強参照になります。オブジェクトが強参照変数によって参照されている場合、そのオブジェクトは常に到達可能な状態にあり、ガベージ コレクション メカニズムによってリサイクルすることは不可能です。オブジェクトが将来使用されない場合でも、JVM はそのオブジェクトを収集しません。 。したがって、強参照は Java メモリ リークの主な原因の 1 つです。

SoftReference

次に、ソフト参照ですが、ソフト参照のみを持つオブジェクトの場合、システム メモリが十分な場合はリサイクルされず、システム メモリ スペースが不足するとリサイクルされます。ソフト参照は、メモリに依存するプログラムでキャッシュとしてよく使用されます。

次に、ソフト参照よりも寿命が短い弱参照があります。弱参照のみを持つオブジェクトの場合、JVM のメモリ領域が十分であるかどうかに関係なく、ガベージ コレクション メカニズムが実行されるとすぐに、オブジェクトによって占有されるメモリは常に占有されます。回収された。メモリ リークの問題を解決できる ThreadLocal は、弱い参照に基づくメモリ リークの問題を解決します。

最後に、仮想参照があります。これは単独で使用することはできず、参照キューと組み合わせて使用​​する必要があります。仮想参照の主な機能は、ガベージ コレクションされているオブジェクトのステータスを追跡することです。ただし、開発では依然として強参照をより頻繁に使用します。

ThreadLocal がアトミック性を確保する方法は、複数のスレッドが重要なリソースを操作することを防ぎ、各スレッドが独自のデータを操作できるようにすることです。

コード

static ThreadLocal tl1 = new ThreadLocal();
static ThreadLocal tl2 = new ThreadLocal();

public static void main(String[] args) {
    
    
    tl1.set("123");
    tl2.set("456");
    Thread t1 = new Thread(() -> {
    
    
        System.out.println("t1:" + tl1.get());
        System.out.println("t1:" + tl2.get());
    });
    t1.start();

    System.out.println("main:" + tl1.get());
    System.out.println("main:" + tl2.get());
}

ThreadLocal 実装原則:

  • 各スレッドにはメンバー変数 ThreadLocalMap が格納されます。
  • ThreadLocal自体はデータを保持するものではなく、ThreadLocalをベースにThreadLocalMapを操作するツールクラスのようなものです。
  • ThreadLocalMap 自体は Entry[] に基づいて実装されていますが、これは 1 つのスレッドが複数の ThreadLocal をバインドできるため、複数のデータを格納する必要がある場合があるため、Entry[] の形式で実装されています。
  • 既存の各 ThreadLocalMap には独自の独立した ThreadLocalMap があり、ThreadLocal オブジェクト自体をキーとして使用して値にアクセスします。
  • ThreadLocalMap のキーは弱参照ですが、弱参照の特徴は、たとえ弱参照があっても GC 時に再利用する必要があることです。これは、ThreadLocal オブジェクトが参照を失った後、キーへの参照が強参照である場合に、ThreadLocal オブジェクトがリサイクルされないようにするためです。

ThreadLocal のメモリ リークの問題:

  • ThreadLocal 参照が失われると、弱い参照のため、キーは GC によってリサイクルされますが、同時にスレッドがリサイクルされていないと、メモリ リークが発生し、メモリ内の値をリサイクルできなくなります。は入手できません。
  • ThreadLocal オブジェクトを使用した後に、エントリを削除するために必要なタイミングで、remove メソッドを呼び出すだけで済みます。

画像.png

可視性

可視性の概念

CPU の位置によって可視性の問題が発生する CPU の処理速度が非常に速い CPU に比べてメインメモリからデータを取得するのが遅すぎる CPU は L1、L2、L3 の 3 レベルのキャッシュを提供L3. メインメモリに移動するたびに、メモリからデータを取得した後、CPU の 3 次キャッシュに格納されます。3 次キャッシュからデータを取得するたびに、効率が確実に向上します。 。

現在、CPU はマルチコアになっており、各スレッドの作業メモリ (CPU の 3 次キャッシュ) は独立していますが、変更を行うと、各スレッドには自分の作業メモリのみが変更されることが通知され、メインメモリに同期されるため、データの不整合の問題が発生します。

画像.png

可視性の問題に対するコードロジック

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
可視性を解決する方法
揮発性の

Volatile は、メンバー変数を変更するために使用されるキーワードです。

属性が volatile に変更されると、現在の属性の操作では CPU キャッシュの使用が許可されず、メイン メモリで操作する必要があることを CPU に伝えるのと同じになります。

volatile のメモリ セマンティクス:

  • volatile 属性が書き込まれます。volatile 変数を書き込むと、JMM は現在のスレッドに対応する CPU キャッシュをメイン メモリに即座にリフレッシュします。
  • volatile 属性が読み取られます。volatile 変数を読み取るとき、JMM は CPU キャッシュ内の対応するメモリを無効に設定し、共有変数をメイン メモリから再読み取る必要があります。

実際、volatile を追加することは、現在の属性の読み取りおよび書き込み操作が CPU キャッシュを使用できないことを CPU に通知することです。volatile で変更された属性は、アセンブリに変換された後、ロック プレフィックスが追加されます。先頭にロックを付けると次の 2 つのことが行われる場合、この命令が実行されます。

  • 現在のプロセッサのキャッシュ ライン データをメイン メモリに書き戻す
  • 書き戻されたデータは、他の CPU コアのキャッシュではそのまま無効になります。

概要: 揮発性とは、CPU がこのデータを操作するたびに、直ちにメイン メモリと同期し、メイン メモリからデータを読み取る必要があることを意味します。

private volatile static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            // ....
        }
        System.out.println("t1线程结束");
    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
同期した

Synchronized は、可視性の問題、同期メモリ セマンティクスも解決できます。

同期された同期コード ブロックまたは同期メソッドが関係する場合、ロック リソースを取得した後、関係する内部変数が CPU キャッシュから削除され、データはメイン メモリから取得する必要があり、ロックが解放された後、CPU はキャッシュ内のデータはすぐにメイン メモリに同期されます。

private static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            synchronized (MiTest.class){
    
    
                //...
            }
            System.out.println(111);
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
ロック

Lock が可視性を確保する方法は、synchronized とはまったく異なります。synchronized は、そのメモリ セマンティクスに基づいて、ロックの取得および解放時に CPU キャッシュをメイン メモリに同期します。

ロックは volatile に基づいて実装されます。Lock ロック内でロックとロックの解放を行うと、volatile によって変更された状態属性が加算または減算されます。

volatile で変更された属性に対して書き込み操作が実行されると、CPU はロック プレフィックスを使用して命令を実行し、CPU は変更されたデータを CPU キャッシュからメイン メモリに即座に同期し、他の属性も即座にメイン メモリに同期します。メインメモリです。他の CPU キャッシュ ラインのこのデータも無効に設定されるため、メイン メモリから再度取得する必要があります。

private static boolean flag = true;
private static Lock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {
    
    
    Thread t1 = new Thread(() -> {
    
    
        while (flag) {
    
    
            lock.lock();
            try{
    
    
                //...
            }finally {
    
    
                lock.unlock();
            }
        }
        System.out.println("t1线程结束");

    });

    t1.start();
    Thread.sleep(10);
    flag = false;
    System.out.println("主线程将flag改为false");
}
最後の

Final-modified 属性は、実行時に変更することはできません。このようにして、可視性が間接的に保証されます。final 属性を読み取るすべてのマルチスレッドは、同じ値を持つ必要があります。

Final は、データが取得されるたびにメイン メモリから読み取られるという意味ではなく、必須ではなく、final と volatile で同時に属性を変更することはできません。

Final によって変更された内容は再度書き込むことができなくなり、volatile は各読み取りおよび書き込みデータがメイン メモリから読み取られることを保証します。volatile は特定のパフォーマンスに影響を与えるため、同時に変更する必要はありません。

画像.png

秩序

秩序の概念

Java では、.java ファイルの内容はコンパイルされ、実行前に CPU が認識できる命令に再変換する必要があります。CPU がこれらの命令を実行するとき、最終結果 (満足のいく結果) に影響を与えることなく実行効率を向上させるために、一部の要件)に応じて、手順が再整理されます。

命令が順不同で実行される理由は、CPU のパフォーマンスを最大化するためです。

Java のプログラムは順不同で実行されます。

Java プログラムは、アウトオブオーダー実行の影響を検証します。

static int a,b,x,y;

public static void main(String[] args) throws InterruptedException {
    
    
    for (int i = 0; i < Integer.MAX_VALUE; i++) {
    
    
        a = 0;
        b = 0;
        x = 0;
        y = 0;

        Thread t1 = new Thread(() -> {
    
    
            a = 1;
            x = b;
        });
        Thread t2 = new Thread(() -> {
    
    
            b = 1;
            y = a;
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        if(x == 0 && y == 0){
    
    
            System.out.println("第" + i + "次,x = "+ x + ",y = " + y);
        }
    }
}

シングルトン モードでは、命令の並べ替えにより問題が発生する可能性があります。

スレッドは初期化されていないオブジェクトを取得する可能性があり、内部プロパティにはデフォルト値があるため、スレッドの使用時に不要な問題が発生する可能性があります。

private static volatile MiTest test;

private MiTest(){
    
    }

public static MiTest getInstance(){
    
    
    // B
    if(test  == null){
    
    
        synchronized (MiTest.class){
    
    

            if(test == null){
    
    
                // A   ,  开辟空间,test指向地址,初始化
                test = new MiTest();
            }
        }
    }
    return test;
}
シリアルのようなもの

as-if-serial セマンティクス:

どのように並べ替えを指定しても、シングルスレッド プログラムの実行結果が変わらないことを保証する必要があります。

また、依存関係がある場合、命令を再配置することはできません。

// 这种情况肯定不能做指令重排序
int i = 0;
i++;

// 这种情况肯定不能做指令重排序
int j = 200;
j * 100;
j + 100;
// 这里即便出现了指令重排,也不可以影响最终的结果,20100
前に起こる

特定のルール:
  1. シングルスレッドの発生前原則: 同じスレッド内で、前の操作を後続の操作の前に書き込みます。
  2. ロックの発生前原理: 同じロックのロック解除操作は、このロックのロック操作よりも前に発生します。
  3. volatile の事前発生の原則: volatile 変数に対する書き込み操作は、この変数に対する操作よりも前に発生します。
  4. 前発生の推移性原理: A が B の前に発生し、B が C の前に発生する場合、A は C の前に発生します。
  5. スレッド起動の先行発生の原則: 同じスレッドの開始メソッドは、このスレッドの他のメソッドよりも先に発生します。
  6. スレッド割り込みの発生前原理: スレッド割り込みメソッドの呼び出しは、割り込みによって送信されたコードを割り込みスレッドが検出する前に発生します。
  7. スレッド終了の発生前原理: スレッド内のすべての操作は、スレッドの終了前に検出されます。
  8. オブジェクト作成の事前発生原則: オブジェクトの初期化は、finalize メソッドが呼び出される前に完了します。
JMM は、上記 8 つの条件が発生しない場合にのみ、命令再配置効果をトリガーしません。

事前発生の原則にあまり注意を払う必要はありません。スレッドセーフなコードを記述できれば十分です。

揮発性の

特定の属性を操作するときにプログラムが命令を再配置しないようにする必要がある場合、事前発生原則を満たすことに加えて、命令の再配置の問題が発生しないように volatile に基づいて属性を変更することもできます。この属性を操作するとき。

volatile はどのようにして命令の再配置を禁止するのでしょうか?

記憶バリアの概念。メモリバリアを命令と考えてください。

2 つの操作の間に前の命令が追加され、この命令により、実行される他の命令が上下に並べ替えられることを回避できます。

おすすめ

転載: blog.csdn.net/qq_44033208/article/details/132434143