ホームページ  >  記事  >  Java  >  Java 同時実行に関するナレッジ ポイントを要約する

Java 同時実行に関するナレッジ ポイントを要約する

WBOY
WBOY転載
2022-03-25 19:16:263244ブラウズ

この記事は、java に関する関連知識を提供します。主に Java の同時実行に関連する問題を紹介し、いくつかの問題を要約します。どれくらいになるかを見てみましょう。お役に立てば幸いです。みんなに。 。

Java 同時実行に関するナレッジ ポイントを要約する

推奨学習: 「java チュートリアル

1. 並列処理と同時実行の違いは何ですか?

オペレーティング システムの観点から見ると、スレッドは CPU 割り当ての最小単位です。

  • 並列処理とは、両方のスレッドが同時に実行されることを意味します。これには、2 つのスレッドをそれぞれ実行するために 2 つの CPU が必要です。
  • 同時実行とは、同時に実行されるのは 1 つだけですが、一定期間内に両方のスレッドが実行されることを意味します。同時実行性の実装は CPU のスレッド切り替えに依存しており、切り替え時間は非常に短いため、基本的にユーザーには認識されません。

Java 同時実行に関するナレッジ ポイントを要約する

# 私たちが食堂に食べ物を取りに行くときと同じように、並行性とは、私たちが複数の窓口に並び、数人のおばさんが同時に食べ物を手に入れることを意味します。 ; 同時進行とは、私たちが 1 つのウィンドウに群がることを意味します, おばさんはこれにスプーン一杯与え、そして急いであれにスプーン一杯与えました。

Java 同時実行に関するナレッジ ポイントを要約する

#2. プロセスとスレッドとは何ですか?

スレッドについて話すには、まずプロセスについて話さなければなりません。

  • プロセス: プロセスは、データ収集に対するコードの実行アクティビティであり、システムにおけるリソースの割り当てとスケジューリングの基本単位です。
  • スレッド: スレッドはプロセスの実行パスです。プロセスには少なくとも 1 つのスレッドが存在します。プロセス内の複数のスレッドはプロセスのリソースを共有します。

オペレーティング システムは、リソースを割り当てるときにプロセスにリソースを割り当てますが、CPU リソースは特別であり、実行のために実際に CPU を占有するのはスレッドであるため、スレッドに割り当てられます。スレッドは CPU 割り当ての基本単位であると述べました。

たとえば、Java では、main 関数を開始すると、実際には JVM プロセスが開始されます。main 関数が配置されているスレッドは、このプロセス内のスレッドであり、メイン スレッドとも呼ばれます。

Java 同時実行に関するナレッジ ポイントを要約する

プロセス内に複数のスレッドが存在します。複数のスレッドはプロセスのヒープおよびメソッド領域のリソースを共有しますが、各スレッドには独自のプログラム カウンタとスタックがあります。

3. スレッドを作成する方法は何通りありますか?

Java でスレッドを作成するには、主に 3 つの方法があります。つまり、Thread クラスの継承、Runnable インターフェイスの実装、および Callable インターフェイスの実装です。

Java 同時実行に関するナレッジ ポイントを要約する

  • Thread クラスを継承し、run() メソッドをオーバーライドし、start() メソッドを呼び出してスレッドを開始します。
public class ThreadTest {

    /**
     * 继承Thread类
     */
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("This is child thread");
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }}
  • 実装 Runnable インターフェイス、run() メソッドをオーバーライドします
public class RunnableTask implements Runnable {
    public void run() {
        System.out.println("Runnable!");
    }

    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        new Thread(task).start();
    }}

上の 2 つは戻り値がありませんが、スレッドの実行結果を取得する必要がある場合はどうすればよいでしょうか。

  • Callable インターフェイスを実装し、call() メソッドをオーバーライドすることで、FutureTask
public class CallerTask implements Callable<string> {
    public String call() throws Exception {
        return "Hello,i am running!";
    }

    public static void main(String[] args) {
        //创建异步任务
        FutureTask<string> task=new FutureTask<string>(new CallerTask());
        //启动线程
        new Thread(task).start();
        try {
            //等待执行完成,并获取返回结果
            String result=task.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }}</string></string></string>

4 を通じてタスク実行の戻り値を取得できます。 start() メソッド run() メソッドが実行されるので、 run() メソッドを直接呼び出してみてはいかがでしょうか。

JVM が start メソッドを実行すると、まずスレッドが作成され、作成された新しいスレッドによってスレッドの run メソッドが実行され、マルチスレッド効果が実現します。

Java 同時実行に関するナレッジ ポイントを要約する

#**なぜ run() メソッドを直接呼び出せないのでしょうか? **Thread の run() メソッドが直接呼び出された場合、run メソッドは依然としてメインスレッドで実行され、これは順次実行と同等であり、マルチスレッド効果が達成されないことも明らかです。

5.スレッドに対して一般的に使用されるスケジューリング方法は何ですか?

Java 同時実行に関するナレッジ ポイントを要約する

#スレッドの待機と通知

オブジェクト クラスには、スレッドの待機と通知に使用できる関数がいくつかあります。

  • wait(): スレッド A が共有変数の wait() メソッドを呼び出すと、スレッド A はブロックされて中断され、次の状況が発生した場合にのみ戻ります。

      ##(1) スレッド A が共有オブジェクトの Notice() または NoticeAll() メソッドを呼び出します;
    • (2) 他のスレッドがスレッド A の割り込みを呼び出します() メソッドの場合、スレッド A は InterruptedException をスローして戻ります。
  • wait(long timeout): このメソッドには wait() メソッドよりもタイムアウト パラメーターが 1 つ多くあります。違いは、スレッド A がshared を呼び出した場合、オブジェクトのwait(long timeout) メソッドが、指定されたタイムアウトミリ秒以内に他のスレッドによって起動されなかった場合でも、このメソッドはタイムアウトのために戻ります。
  • wait(long timeout, int nanos)。内部的に wait(long timeout) 関数を呼び出します。
  • 上記はスレッドの待機方法であり、スレッドを起動するには主に 2 つの方法があります。
    • notify(): スレッド A が共有オブジェクトの notify() メソッドを呼び出した後、共有変数で wait 一連のメソッドを呼び出した後に一時停止されていたスレッドを起動します。共有変数を待機している複数のスレッドが存在する可能性があり、どの待機スレッドが起動されるかはランダムです。
    • notifyAll(): 共有変数でブロックされているスレッドを起動する共有変数でのnotify() 関数の呼び出しとは異なり、notifyAll() メソッドは、共有変数でブロックされているすべてのスレッドを起動します。 to call wait. 一連のメソッドにより中断されたスレッド。

    Thread クラスは、待機するためのメソッドも提供します。

    • join(): スレッド A が thread.join() ステートメントを実行する場合、その意味は現在のスレッド A は、

      が thread.join() から戻る前に、スレッド thread が終了するのを待ちます。

    Thread sleep

    • sleep(long millis): Thread クラスの静的メソッド。スレッド A の実行時に呼び出されます。スレッドのスリープ方法では、スレッド A は指定された時間の間実行権を一時的に放棄しますが、スレッド A が所有するロックなどの監視リソースは保持されたままで放棄されません。指定されたスリープ時間が経過すると、関数は正常に戻り、CPU スケジューリングに参加し、CPU リソースを取得した後も実行を継続します。

    Give priority

    • yield(): Thread クラスの静的メソッドスレッドが yield メソッドを呼び出すと、実際にはそれがヒントになります現在のスレッドが CPU の放棄を要求していることをスレッド スケジューラに通知しますが、スレッド スケジューラはこのヒントを無条件に無視できます。

    スレッド中断

    Java のスレッド中断はスレッド間の協調モードであり、スレッドの中断フラグを設定してもスレッドを直接終了することはできません。中断されたスレッドは、中断ステータスに応じて独自に処理します。

    • voidinterrupt(): スレッドを中断します。たとえば、スレッド A の実行中に、スレッド B は、interrupt() メソッドを呼び出して、スレッドの割り込みフラグを true に設定し、すぐに戻ることができます。フラグの設定はフラグを設定するだけであり、スレッド A は実際には中断されず、実行を継続します。
    • boolean isInterrupted() メソッド: 現在のスレッドが中断されているかどうかを検出します。
    • boolean Interrupted() メソッド: 現在のスレッドが中断されているかどうかを検出します。isInterrupted とは異なり、このメソッドは、現在のスレッドが中断されたことが判明した場合、中断フラグをクリアします。

    6. スレッドにはいくつの状態がありますか?

    Java では、スレッドには 6 つの状態があります:

    State Description
    ##NEW 初期状態: スレッドは作成されましたが、start() メソッドはまだ呼び出されていません
    RUNNABLE 実行状態 : Java スレッドは通常、オペレーティング システムでの準備完了と実行の 2 つの状態を「実行中」と呼びます。
    BLOCKED ブロッキング状態:スレッドはロックでブロックされています
    WAITING 待機状態: スレッドが待機状態に入ったことを示します。この状態に入ることは、現在のスレッドが他のスレッドを待機する必要があることを意味します。特定のアクション (通知または中断) を行うためのスレッド
    TIME_WAITING タイムアウト待機ステータス: このステータスは WAITIND とは異なり、指定された時間に自動的に戻ることができます。
    TERMINATED 終了ステータス: 現在のスレッドが実行を完了したことを示します

    独自のライフサイクルでは、スレッドは固定された状態にありませんが、コードが実行されるとさまざまな状態の間で切り替わります。Java スレッドの状態は、図に示すように変化します:

    Java 同時実行に関するナレッジ ポイントを要約する

    7. スレッド コンテキストの切り替えとは何ですか?

    マルチスレッドを使用する目的は CPU を最大限に活用することですが、同時実行性は実際には 1 つの CPU で複数のスレッドを処理することであることがわかっています。

    Java 同時実行に関するナレッジ ポイントを要約する

    ユーザーに複数のスレッドが同時に実行されていると感じさせるために、タイム スライスのローテーションを使用して CPU リソースが割り当てられます。つまり、各スレッドにタイム スライスが割り当てられます。 . CPU は、タイム スライス内でタスクを実行するために占有されます。スレッドがタイム スライスを使い切ると、スレッドは準備完了状態になり、他のスレッドが CPU を占有できるようになります。これがコンテキスト スイッチです。

    Java 同時実行に関するナレッジ ポイントを要約する

    8. デーモン スレッドについて理解していますか?

    Java のスレッドは、デーモン スレッド (デーモン スレッド) とユーザー スレッド (ユーザー スレッド) の 2 つのカテゴリに分類されます。

    JVM の起動時に main 関数が呼び出されます。main 関数が配置されているプロセスはユーザー スレッドです。実際、ガベージ コレクション スレッドなど、多くのデーモン スレッドも JVM 内で起動されます。

    それでは、デーモン スレッドとユーザー スレッドの違いは何でしょうか?違いの 1 つは、最後の非デーモン スレッドがワープすると、現在デーモン スレッドが存在するかどうかに関係なく、JVM が正常に終了することです。つまり、デーモン スレッドが終了するかどうかは JVM の終了には影響しません。つまり、1 つのユーザー スレッドが終了しない限り、通常の状況では JVM は終了しません。

    9. スレッド間の通信方法は何ですか?

    Java 同時実行に関するナレッジ ポイントを要約する

    • volatile キーワードと同期キーワード

    キーワード volatile は、フィールド (メンバー変数) を変更するために使用できます。これは、変数へのアクセスは共有メモリから取得する必要があり、変数への変更は共有メモリに同期的にフラッシュバックする必要があることをプログラムに通知するため、すべてのスレッドによる変数アクセスの可視性を確保できます。

    キーワード synchronized は、メソッドまたは同期ブロックの形式を変更するために使用できます。これは主に、複数のスレッドがメソッドまたは同期ブロック内で同時に 1 つのスレッドのみを持つことができることを保証します。変数アクセスの可視性と排他性。
    • 待機/通知メカニズム

    スレッドの変更は、Java の組み込み待機/通知メカニズム (wait()/notify() を通じて実装できます。 )) オブジェクトの値、および別のスレッドが変更を感知し、対応する操作を実行します。
    • パイプライン入出力ストリーム

    パイプライン入出力ストリームは、通常のファイル入出力ストリームやネットワーク入出力ストリームとは異なります。その理由は、主にスレッド間のデータ伝送に使用され、伝送媒体がメモリであるためです。

    パイプ入出力ストリームには主に、PipedOutputStream、PipedInputStream、PipedReader、および PipedWriter の 4 つの特定の実装が含まれます。最初の 2 つはバイト指向で、後の 2 つは文字指向です。
    • Thread.join() を使用する

    スレッド A が thread.join() ステートメントを実行する場合、意味は次のとおりです: 現在のスレッド A thread.join() から戻る前に、スレッド thread が終了するのを待ちます。 。 join() メソッドに加えて、スレッド Thread は、タイムアウト特性を持つ 2 つのメソッド、join(long millis) と join(long millis, int nanos) も提供します。
    • ThreadLocal の使用

    ThreadLocal、つまりスレッド変数は、ThreadLocal オブジェクトをキーとして、任意のオブジェクトをキーとして持つ記憶構造です。価値。この構造体はスレッドに付加されます。これは、スレッドが ThreadLocal オブジェクトに基づいて、このスレッドにバインドされた値をクエリできることを意味します。

    set(T) メソッドを使用して値を設定し、現在のスレッドの get() メソッドを使用して元の設定値を取得できます。

    マルチスレッドに関しては、代替印刷、銀行振込、生産モデルと消費モデルなどの筆記試験問題も出題される可能性が高くなります。後日、Laosan が公開する予定です。一般的な質問を別の号で確認し、マルチスレッドの筆記試験問題を確認します。

    ThreadLocal

    ThreadLocal には、実際には多くのアプリケーション シナリオはありませんが、マルチスレッド、データ構造、JVM が含まれており、何千回も攻撃を受けてきたベテランのインタビューです。 . 質問することができます。より多くのポイントを持っている場合は、ポイントを獲得する必要があります。

    10.ThreadLocal とは何ですか?

    ThreadLocal。スレッド ローカル変数です。 ThreadLocal 変数を作成すると、この変数にアクセスする各スレッドは、この変数のローカル コピーを持つことになります。複数のスレッドがこの変数を操作する場合、実際には独自のローカル メモリ内で変数を操作するため、スレッドの分離が実現されます。スレッドを回避する関数安全性の問題。

    Java 同時実行に関するナレッジ ポイントを要約する

    ###
    • 创建

    创建了一个ThreadLoca变量localVariable,任何一个线程都能并发访问localVariable。

//创建一个ThreadLocal变量public static ThreadLocal<string> localVariable = new ThreadLocal();</string>
  • 写入

线程可以在任何地方使用localVariable,写入变量。

localVariable.set("鄙人三某”);
  • 读取

线程在任何地方读取的都是它写入的变量。

localVariable.get();

11.你在工作中用到过ThreadLocal吗?

有用到过的,用来做用户信息上下文的存储。

我们的系统应用是一个典型的MVC架构,登录后的用户每次访问接口,都会在请求头中携带一个token,在控制层可以根据这个token,解析出用户的基本信息。那么问题来了,假如在服务层和持久层都要用到用户信息,比如rpc调用、更新用户获取等等,那应该怎么办呢?

一种办法是显式定义用户相关的参数,比如账号、用户名……这样一来,我们可能需要大面积地修改代码,多少有点瓜皮,那该怎么办呢?

这时候我们就可以用到ThreadLocal,在控制层拦截请求把用户信息存入ThreadLocal,这样我们在任何一个地方,都可以取出ThreadLocal中存的用户数据。

Java 同時実行に関するナレッジ ポイントを要約する

很多其它场景的cookie、session等等数据隔离也都可以通过ThreadLocal去实现。

我们常用的数据库连接池也用到了ThreadLocal:

  • 数据库连接池的连接交给ThreadLoca进行管理,保证当前线程的操作都是同一个Connnection。

12.ThreadLocal怎么实现的呢?

我们看一下ThreadLocal的set(T)方法,发现先获取到当前线程,再获取ThreadLocalMap,然后把元素存到这个map中。

    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //讲当前元素存入map
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

ThreadLocal实现的秘密都在这个ThreadLocalMap了,可以Thread类中定义了一个类型为ThreadLocal.ThreadLocalMap的成员变量threadLocals

public class Thread implements Runnable {
   //ThreadLocal.ThreadLocalMap是Thread的属性
   ThreadLocal.ThreadLocalMap threadLocals = null;}

ThreadLocalMap既然被称为Map,那么毫无疑问它是型的数据结构。我们都知道map的本质是一个个形式的节点组成的数组,那ThreadLocalMap的节点是什么样的呢?

        static class Entry extends WeakReference<threadlocal>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            //节点类
            Entry(ThreadLocal> k, Object v) {
                //key赋值
                super(k);
                //value赋值
                value = v;
            }
        }</threadlocal>

这里的节点,key可以简单低视作ThreadLocal,value为代码中放入的值,当然实际上key并不是ThreadLocal本身,而是它的一个弱引用,可以看到Entry的key继承了 WeakReference(弱引用),再来看一下key怎么赋值的:

    public WeakReference(T referent) {
        super(referent);
    }

key的赋值,使用的是WeakReference的赋值。

Java 同時実行に関するナレッジ ポイントを要約する

所以,怎么回答ThreadLocal原理?要答出这几个点:

  • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。
  • 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
  • ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。

13.ThreadLocal 内存泄露是怎么回事?

我们先来分析一下使用ThreadLocal时的内存,我们都知道,在JVM中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。

所以呢,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。

Java 同時実行に関するナレッジ ポイントを要約する

ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用。

“弱引用:只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存。”

那么现在问题就来了,弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题

那怎么解决内存泄漏问题呢?

很简单,使用完ThreadLocal后,及时调用remove()方法释放内存空间。

ThreadLocal localVariable = new ThreadLocal();try {
    localVariable.set("鄙人三某”);
    ……} finally {
    localVariable.remove();}

那为什么key还要设计成弱引用?

key设计成弱引用同样是为了防止内存泄漏。

假如key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLoca的强引用就没有了,但是此时key还强引用指向ThreadLoca,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。

14.ThreadLocalMap的结构了解吗?

ThreadLocalMap虽然被叫做Map,其实它是没有实现Map接口的,但是结构还是和HashMap比较类似的,主要关注的是两个要素:元素数组散列方法

Java 同時実行に関するナレッジ ポイントを要約する

  • 元素数组

    一个table数组,存储Entry类型的元素,Entry是ThreaLocal弱引用作为key,Object作为value的结构。

 private Entry[] table;
  • 散列方法

    散列方法就是怎么把对应的key映射到table数组的相应下标,ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table数组长度减一&运算(相当于取余)。

int i = key.threadLocalHashCode & (table.length - 1);

这里的threadLocalHashCode计算有点东西,每创建一个ThreadLocal对象,它就会新增0x61c88647,这个值很特殊,它是斐波那契数 也叫 黄金分割数hash增量为 这个数字,带来的好处就是 hash 分布非常均匀

    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

15.ThreadLocalMap怎么解决Hash冲突的?

我们可能都知道HashMap使用了链表来解决冲突,也就是所谓的链地址法。

ThreadLocalMap没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另外一种方式——开放定址法。开放定址法是什么意思呢?简单来说,就是这个坑被人占了,那就接着去找空着的坑。

Java 同時実行に関するナレッジ ポイントを要約する

如上图所示,如果我们插入一个value=27的数据,通过 hash计算后应该落入第 4 个槽位中,而槽位 4 已经有了 Entry数据,而且Entry数据的key和当前不相等。此时就会线性向后查找,一直找到 Entry为 null的槽位才会停止查找,把元素放到空的槽中。

在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该槽位Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置。

16.Java 同時実行に関するナレッジ ポイントを要約する机制了解吗?

在ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:

if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

再着看rehash()具体实现:这里会先去清理过期的Entry,然后还要根据条件判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4来决定是否需要扩容。

private void rehash() {
    //清理过期Entry
    expungeStaleEntries();

    //扩容
    if (size >= threshold - threshold / 4)
        resize();}//清理过期Entryprivate void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    for (int j = 0; j <p>接着看看具体的<code>resize()</code>方法,扩容后的<code>newTab</code>的大小为老数组的两倍,然后遍历老的table数组,散列方法重新计算位置,开放地址解决冲突,然后放到新的<code>newTab</code>,遍历完成之后,<code>oldTab</code>中所有的<code>entry</code>数据都已经放入到<code>newTab</code>中了,然后table引用指向<code>newTab</code></p><p><img src="https://img.php.cn/upload/article/000/000/067/6137d48077cbb320beee2007e8763d69-16.png" alt="Java 同時実行に関するナレッジ ポイントを要約する"></p><p>具体代码:</p><p><img src="https://img.php.cn/upload/article/000/000/067/85310a4f8d2bb86283cd76fb2542424d-17.png" alt="ThreadLocalMap resize"></p><h2>17.父子线程怎么共享数据?</h2><p>父线程能用ThreadLocal来给子线程传值吗?毫无疑问,不能。那该怎么办?</p><p>这时候可以用到另外一个类——<code>InheritableThreadLocal</code>。</p><p>使用起来很简单,在主线程的InheritableThreadLocal实例设置值,在子线程中就可以拿到了。</p><pre class="brush:php;toolbar:false">public class InheritableThreadLocalTest {
    
    public static void main(String[] args) {
        final ThreadLocal threadLocal = new InheritableThreadLocal();
        // 主线程
        threadLocal.set("不擅技术");
        //子线程
        Thread t = new Thread() {
            @Override
            public void run() {
                super.run();
                System.out.println("鄙人三某 ," + threadLocal.get());
            }
        };
        t.start();
    }}

那原理是什么呢?

原理很简单,在Thread类里还有另外一个变量:

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

在Thread.init的时候,如果父线程的inheritableThreadLocals不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals

        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)

18.说一下你对Java 同時実行に関するナレッジ ポイントを要約する(JMM)的理解?

Java 同時実行に関するナレッジ ポイントを要約する(Java Memory Model,JMM),是一种抽象的模型,被定义出来屏蔽各种硬件和操作系统的内存访问差异。

JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。

Java 同時実行に関するナレッジ ポイントを要約する的抽象图:

Java 同時実行に関するナレッジ ポイントを要約する

本地内存是JMM的 一个抽象概念,并不真实存在。它其实涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

Java 同時実行に関するナレッジ ポイントを要約する

图里面的是一个双核 CPU 系统架构 ,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有 CPU 共享的二级缓存。 那么 Java 内存模型里面的工作内存,就对应这里的 Ll 缓存或者 L2 缓存或者 CPU 寄存器。

19.说说你对原子性、可见性、有序性的理解?

原子性、有序性、可见性是并发编程中非常重要的基础概念,JMM的很多技术都是围绕着这三大特性展开。

  • 原子性:原子性指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。
  • 可见性:可见性指的是一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改。
  • 有序性:有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。

分析下面几行代码的原子性?

int i = 2;int j = i;i++;i = i + 1;
  • 第1句是基本类型赋值,是原子性操作。
  • 第2句先读i的值,再赋值到j,两步操作,不能保证原子性。
  • 第3和第4句其实是等效的,先读取i的值,再+1,最后赋值到i,三步操作了,不能保证原子性。

原子性、可见性、有序性都应该怎么保证呢?

  • 原子性:JMM只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用synchronized
  • 可见性:Java是利用volatile关键字来保证可见性的,除此之外,finalsynchronized也能保证可见性。
  • 有序性:synchronized或者volatile都可以保证多线程之间操作的有序性。

20.那说说什么是指令重排?

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序,如图:

Java 同時実行に関するナレッジ ポイントを要約する

我们比较熟悉的双重校验单例模式就是一个经典的指令重排的例子,Singleton instance=new Singleton();对应的JVM指令分为三步:分配内存空间–>初始化对象—>对象指向分配的内存空间,但是经过了编译器的指令重排序,第二步和第三步就可能会重排序。

Java 同時実行に関するナレッジ ポイントを要約する

JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

21.指令重排有限制吗?happens-before了解吗?

指令重排也是有一些限制的,有两个规则happens-beforeas-if-serial来约束。

happens-before的定义:

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法

happens-before和我们息息相关的有六大规则:

Java 同時実行に関するナレッジ ポイントを要約する

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的 ThreadB.start()操作happens-before于线程B中的任意操作。
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before于线程A从ThreadB.join()操作成功返回。

22.as-if-serial又是什么?单线程的程序一定是顺序的吗?

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例。

double pi = 3.14;   // Adouble r = 1.0;   // B double area = pi * r * r;   // C

上面3个操作的数据依赖关系:

Java 同時実行に関するナレッジ ポイントを要約する

A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。

所以最终,程序可能会有两种执行顺序:

Java 同時実行に関するナレッジ ポイントを要約する

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器共同编织了这么一个“楚门的世界”:单线程程序是按程序的“顺序”来执行的。as- if-serial语义使单线程情况下,我们不需要担心重排序的问题,可见性的问题。

23.volatile实现原理了解吗?

volatile有两个作用,保证可见性有序性

volatile怎么保证可见性的呢?

相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,它没有上下文切换的额外开销成本。

volatile可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存 当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。

例如,我们声明一个 volatile 变量 volatile int x = 0,线程A修改x=1,修改完之后就会把新的值刷新回主内存,线程B读取x的时候,就会清空本地内存变量,然后再从主内存获取最新值。

Java 同時実行に関するナレッジ ポイントを要約する

volatile怎么保证有序性的呢?

重排序可以分为编译器重排序和处理器重排序,valatile保证有序性,就是通过分别限制这两种类型的重排序。

Java 同時実行に関するナレッジ ポイントを要約する

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

  1. 在每个volatile写操作的前面插入一个StoreStore屏障
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障
  4. 在每个volatile读操作的后面插入一个LoadStore屏障

Java 同時実行に関するナレッジ ポイントを要約する

24.synchronized用过吗?怎么使用?

synchronized经常用的,用来保证代码的原子性。

synchronized主要有三种用法:

  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() {
  //业务代码}
  • 修饰静态方法:也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得当前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有⼀份)。

    如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。

synchronized void staic method() {
 //业务代码}
  • 修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized(类.class) 表示进⼊同步代码前要获得 当前 class 的锁
synchronized(this) {
 //业务代码}

25.synchronized的实现原理?

synchronized是怎么加锁的呢?

我们使用synchronized的时候,发现不用自己去lock和unlock,是因为JVM帮我们把这个事情做了。

  1. synchronized修饰代码块时,JVM采用monitorentermonitorexit两个指令来实现同步,monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指向同步代码块的结束位置。

    反编译一段synchronized修饰代码块代码,javap -c -s -v -l SynchronizedDemo.class,可以看到相应的字节码指令。

Java 同時実行に関するナレッジ ポイントを要約する

  1. Java 同時実行に関するナレッジ ポイントを要約する时,JVM采用ACC_SYNCHRONIZED标记符来实现同步,这个标识指明了该方法是一个同步方法。

    同样可以写段代码反编译看一下。

Java 同時実行に関するナレッジ ポイントを要約する

synchronized锁住的是什么呢?

monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor实现的。

实例对象结构里有对象头,对象头里面有一块结构叫Mark Word,Mark Word指针指向了monitor

所谓的Monitor其实是一种同步工具,也可以说是一种同步机制。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,可以叫做内部锁,或者Monitor锁。

ObjectMonitor的工作原理:

  • ObjectMonitor有两个队列:_WaitSet、_EntryList,用来保存ObjectWaiter 对象列表。
  • _owner,获取 Monitor 对象的线程进入 _owner 区时, _count + 1。如果线程调用了 wait() 方法,此时会释放 Monitor 对象, _owner 恢复为空, _count - 1。同时该等待线程进入 _WaitSet 中,等待被唤醒。
ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0;  //锁的重入次数
    _object       = NULL;
    _owner        = NULL;  // 指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

可以类比一个去医院就诊的例子[18]:

  • 首先,患者在门诊大厅前台或自助挂号机进行挂号

  • 随后,挂号结束后患者找到对应的诊室就诊

    • 诊室每次只能有一个患者就诊;
    • 如果此时诊室空闲,直接进入就诊;
    • 如果此时诊室内有其它患者就诊,那么当前患者进入候诊室,等待叫号;
  • 就诊结束后,走出就诊室,候诊室的下一位候诊患者进入就诊室。

Java 同時実行に関するナレッジ ポイントを要約する

这个过程就和Monitor机制比较相似:

  • 外来ホール: エントリーされるすべてのスレッドが資格を得るには、まず エントリー セットに登録する必要があります;
  • 面会室: 治療室 **_Owner** に治療できるスレッドは 1 つだけです。治療後、スレッドは自動的に退出します。
  • 待機室 : 治療室終了時ビジーです。待機するには を入力してください。エリア (待機セット) 、治療室が空くと、待機エリア (待機セット)

# から新しいスレッドが呼び出されます。 Java Montior机制

## したがって、どの同期がロックされているかがわかります:

#monitorenter. 同期フラグ ACC_SYNCHRONIZED があると判断した後、最初にこのメソッドに入ったスレッドが所有者を持ちます。このとき、カウンタは 1 です。
  • monitorexit は、実行後に終了すると、カウンター -1 が 0 に戻り、他の開始スレッドによって取得されます。
  • 26. アトミック性に加えて、同期した可視性、秩序性、再入性を実現するにはどうすればよいでしょうか?

同期により可視性はどのように確保されるのでしょうか?

スレッドがロックされる前に、ワーキングメモリ内のシェア変数の値がクリアされるため、シェア変数を使用する場合は、メインスレッドから最新の値を再読み込みする必要があります。メモリ。
  • スレッドがロックされると、他のスレッドはメイン メモリ内の共有変数を取得できなくなります。
  • スレッドのロックを解除する前に、共有変数の最新の値をメイン メモリに更新する必要があります。
同期によって順序性はどのように確保されるのでしょうか?

同期されたコード ブロックは排他的であり、一度に 1 つのスレッドのみが所有できるため、同期により、コードが 1 つのスレッドによって同時に実行されることが保証されます。

as-if-serial セマンティクスが存在するため、シングルスレッド プログラムでは最終結果が適切であることを保証できますが、命令が再配置されないという保証はありません。

したがって、synchronized によって保証される順序は実行結果の順序であり、命令の並べ替えを防ぐ順序ではありません。

同期はどのようにして再入可能を実現しますか?

synchronized はリエントラント ロックです。つまり、スレッドは保持しているオブジェクト ロックのクリティカル リソースを 2 回要求できます。この状況はリエントラント ロックと呼ばれます。

synchronized オブジェクトをロックするときにカウンターがあります。スレッドがロックを取得した回数が記録されます。対応するコード ブロックが実行された後、カウンターがクリアされるまでカウンターは -1 になります。ロックが解除されます。

理由は、リエントラントであるためです。これは、同期ロック オブジェクトにはカウンターがあり、スレッドがロックを取得した後は 1 ずつカウントされ、スレッドが実行を完了すると、ロックが解除されてロックが解放されるまで -1 ずつカウントされるためです。

27. ロックのアップグレード?同期最適化について理解していますか?

ロックを解除してアップグレードするには、まずさまざまなロックのステータスを把握する必要があります。このステータスは何を指しますか?

Java オブジェクトのヘッダーには、

Mark Word

マーク フィールドという構造があり、ロック状態の変化に応じてこの構造も変化します。 64 ビット仮想マシン Mark Word は 64 ビットです。ステータスの変化を見てみましょう:

Mark Word变化Mark Word はオブジェクトの実行データを保存します

ハッシュ コード、GC 生成期間、ロック ステータス フラグ、バイアス タイムスタンプ (エポック)

など、それ自体。

同期ではどのような最適化が行われましたか?

JDK1.6 より前では、synchronized の実装は ObjectMonitor の開始と終了を直接呼び出していました。この種のロックは
Heavyweight lock

と呼ばれていました。 JDK6 以降、HotSpot 仮想マシン開発チームは、同期のパフォーマンスを向上させるために、適応スピン、ロックの削除、ロックの粗密化、軽量ロック、バイアスされたロックなどの最適化戦略を追加するなど、Java でロックを最適化しました。

    バイアス ロック: 競合がない場合、現在のスレッド ポインタは Mark Word にのみ保存され、CAS 操作は実行されません。
  • 軽量ロック: マルチスレッドの競合がない場合、重量ロックと比較して、オペレーティング システムのミューテックスによるパフォーマンスの消費が軽減されます。ただし、ロックの競合が発生すると、ミューテックス自体のオーバーヘッドに加えて、CAS 操作の追加のオーバーヘッドも発生します。
  • スピン ロック: 不必要な CPU コンテキストの切り替えを削減します。軽量ロックを重量ロックにアップグレードする場合、スピン ロック方式が使用されます。
  • ロックの粗密化: 複数の連続したロック操作とロック解除操作をまとめて接続し、より広い範囲のロックに拡張します。
  • ロックの削除: 仮想マシンのジャストインタイム コンパイラの実行中に、一部のコードで同期が必要だが、共有データの競合が発生する可能性が低いと検出されたロックが削除されます。
ロックのアップグレードのプロセスは何ですか?

ロックのアップグレード方向: ロックなし –> バイアス ロック –> 軽量ロック –> 重量ロック。この方向は基本的に元に戻すことはできません。

Java 同時実行に関するナレッジ ポイントを要約する アップグレード プロセスを見てみましょう:

バイアス ロック:

バイアス ロックの取得:

  1. バイアス可能な状態かどうかを判断します – MarkWord のロック フラグが '01' かどうか、バイアスされたロックが '1' かどうか
  2. バイアス可能な状態の場合は、スレッドがID は現在のスレッドです。そうである場合はステップ '5' に進み、そうでない場合はステップ '3' に進みます。
  3. CAS 操作を通じてロックを競合します。競合が成功した場合は、MarkWord のスレッド ID を現在のスレッド ID を取得してから '5 ' を実行します。競合が失敗した場合は、'4'を実行します。
  4. CAS バイアス ロックの取得に失敗した場合、競合が示されます。セーフポイントに到達すると、バイアスされたロックを取得したスレッドは一時停止され、バイアスされたロックは軽量ロックにアップグレードされますその後、セーフポイントでブロックされたスレッドは同期コード ブロックの実行を継続します
  5. 実行同期コード

バイアスされたロックの取り消し:

  1. バイアスされたロックは積極的に解放 (取り消し) されず、単に解放されるだけです。他のスレッドが競合すると解放されます。取り消しを実行するには、現在バイアスされたロックを保持しているスレッドのスタック状態を知る必要があるため、セーフポイントまで実行を待つ必要があります。このとき、バイアスされたロックを保持しているスレッド (T)状況は 2 つあります: '2' と '3';
  2. 取り消し----T スレッドが同期コード ブロックを終了するか、もはや生きていない場合、バイアス ロックは直接取り消され、ロックフリー状態----状態がしきい値 20 に達すると、バッチ再バイアスが実行されます
  3. アップグレード----T スレッドがまだ同期コード ブロック内にあるため、バイアスされたロック # T スレッドの ## が軽量ロック にアップグレードされ、現在のスレッドが軽量ロック状態でロックを実行します。 取得手順 ---- ステータスがしきい値 40 に達した場合、バッチ取り消しを実行します
軽量ロック:

軽量ロックの取得:

    ロック操作を実行するとき、JVM は重量ロックが取得されたかどうかを判断します。そうでない場合、現在のスレッド スタック フレームにロックのロック レコードとしてスペースが描画され、ロック オブジェクトは MarkWord がロック レコードにコピーされます
  1. コピーが成功した後、jvm はCAS 操作は、オブジェクト ヘッダー MarkWord をロック レコードへのポインターに更新し、ロック レコード内の所有者ポインターをオブジェクト ヘッダーの MarkWord にポイントします。成功した場合は「3」を実行し、そうでない場合は「4」を実行します。
  2. 更新が成功した場合、現在のスレッドがオブジェクト ロックを保持し、オブジェクト MarkWord ロック フラグが「00」に設定されます。これは、これが意味します。オブジェクトは軽量状態にあります。レベル ロック ステータス
  3. 更新に失敗しました。jvm はまず、オブジェクト MarkWord が現在のスレッド スタック フレーム内のロック レコードをポイントしているかどうかを確認します。ポイントしている場合は、'5' を実行し、そうでない場合は、'4' を実行します。 '
  4. ロックが繰り返されることを示します Enter; その後、ロック レコードの最初の部分 (置き換えられたマーク ワード) が現在のスレッド スタック フレームに null として追加され、マーク ワード ロック オブジェクトを指します。再入カウンタ。
  5. ロック オブジェクトが他のスレッドによってプリエンプトされたことを示します。その後、
  6. スピン待機 (デフォルトは 10 回)、待機数がしきい値に達し、ロックが取得されなかった場合は、 は重量レベル ロックにアップグレードされます
一般的に簡略化されたアップグレード プロセス:

Java 同時実行に関するナレッジ ポイントを要約する

完全なアップグレード プロセス:

synchronized 锁升级过程-来源参考[14]

28. synchronized ロックと ReentrantLock の違いについて話しますか?

この質問には、ロックの実装、機能特性、パフォーマンスなどのいくつかの側面から答えることができます。

  • ロックの実装: 同期は Java の鍵です。 Word 言語。JVM に基づいて実装されています。 ReentrantLock は、JDK の API レベルに基づいて実装されます (通常、lock() メソッドとunlock() メソッドは try/finally ステートメント ブロックと組み合わされます。)
  • パフォーマンス: JDK1.6 ではlock 最適化する前は、synchronized のパフォーマンスは ReenTrantLock よりもはるかに悪かった。しかし、JDK6以降はアダプティブスピンやロック解除などが追加されており、両者の性能はほぼ同等となっています。
  • 機能的特徴: ReentrantLock は、割り込み可能な待機、公平なロック、選択的通知など、同期よりも高度な機能をいくつか追加します。
      ReentrantLock は、ロックを待機しているスレッドに割り込むことができるメカニズムを提供します。このメカニズムは、lock.lockInterruptibly() によって実装されます。
    • ReentrantLock は、公平なロックか不公平なロックかを指定できます。そして、同期は不公平なロックにしかなりません。いわゆるフェアロックとは、最初に待機しているスレッドが最初にロックを取得することを意味します。
    • synchronized は、wait() および Notice()/notifyAll() メソッドと組み合わされて、待機/通知メカニズムを実装します。ReentrantLock クラスは、Condition インターフェイスと newCondition() メソッドを利用して実装されます。
    • ReentrantLock は、ロックとロックの解放を行うために手動で宣言する必要があります。通常は、ロックを解放する Finally と組み合わせて宣言する必要があります。同期では手動でロックを解除する必要はありません。
次の表に、2 つのロックの違いを示します:

Java 同時実行に関するナレッジ ポイントを要約する

29. AQS についてどのくらい知っていますか? ?

AbstractQueuedSynchronizer 抽象同期キュー (AQS と呼ばれる) は、Java 同時実行パッケージの基盤です。同時実行パッケージ内のロックは、AQS に基づいて実装されます。

  • AQS是基于一个FIFO的双向队列,其内部定义了一个节点类Node,Node 节点内部的 SHARED 用来标记该线程是获取共享资源时被阻挂起后放入AQS 队列的, EXCLUSIVE 用来标记线程是 取独占资源时被挂起后放入AQS 队列
  • AQS 使用一个 volatile 修饰的 int 类型的成员变量 state 来表示同步状态,修改同步状态成功即为获得锁,volatile 保证了变量在多线程之间的可见性,修改 State 值时通过 CAS 机制来保证修改的原子性
  • 获取state的方式分为两种,独占方式和共享方式,一个线程使用独占方式获取了资源,其它线程就会在获取失败后被阻塞。一个线程使用共享方式获取了资源,另外一个线程还可以通过CAS的方式进行获取。
  • 如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁的分配,AQS 中会将竞争共享资源失败的线程添加到一个变体的 CLH 队列中。

Java 同時実行に関するナレッジ ポイントを要約する先简单了解一下CLH:Craig、Landin and Hagersten 队列,是 单向链表实现的队列。申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现 前驱节点释放了锁就结束自旋

Java 同時実行に関するナレッジ ポイントを要約する

AQS 中的队列是 CLH 变体的虚拟双向队列,通过将每条请求共享资源的线程封装成一个节点来实现锁的分配:

AQS变种Java 同時実行に関するナレッジ ポイントを要約する

AQS 中的 CLH 变体等待队列拥有以下特性:

  • AQS 中队列是个双向链表,也是 FIFO 先进先出的特性
  • 通过 Head、Tail 头尾两个节点来组成队列结构,通过 volatile 修饰保证可见性
  • Head 指向节点为已获得锁的节点,是一个虚拟节点,节点本身不持有具体线程
  • 获取不到同步状态,会将节点进行自旋获取锁,自旋一定次数失败后会将线程阻塞,相对于 CLH 队列性能较好

ps:AQS源码里面有很多细节可问,建议有时间好好看看AQS源码。

30.ReentrantLock实现原理?

ReentrantLock 是可重入的独占锁,只能有一个线程可以获取该锁,其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列里面。

看看ReentrantLock的加锁操作:

    // 创建非公平锁
    ReentrantLock lock = new ReentrantLock();
    // 获取锁操作
    lock.lock();
    try {
        // 执行代码逻辑
    } catch (Exception ex) {
        // ...
    } finally {
        // 解锁操作
        lock.unlock();
    }

new ReentrantLock()构造函数默认创建的是非公平锁 NonfairSync。

公平锁 FairSync

  1. 公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁
  2. 公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大

非公平锁 NonfairSync

  • 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁
  • 非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁

默认创建的对象lock()的时候:

  • 如果锁当前没有被其它线程占用,并且当前线程之前没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为1 ,然后直接返回。如果当前线程之前己经获取过该锁,则这次只是简单地把 AQS 的状态值加1后返回。
  • 如果该锁己经被其他线程持有,非公平锁会尝试去获取锁,获取失败的话,则调用该方法线程会被放入 AQS 队列阻塞挂起。

ReentrantLock 非公平锁加锁流程简图

31.ReentrantLock怎么实现公平锁的?

new ReentrantLock()构造函数默认创建的是非公平锁 NonfairSync

public ReentrantLock() {
    sync = new NonfairSync();}

同时也可以在创建锁构造函数中传入具体参数创建公平锁 FairSync

ReentrantLock lock = new ReentrantLock(true);--- ReentrantLock// true 代表公平锁,false 代表非公平锁public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();}

FairSync、NonfairSync 代表公平锁和非公平锁,两者都是 ReentrantLock 静态内部类,只不过实现不同锁语义。

非公平锁和公平锁的两处不同:

  1. 不正なロックに対して lock を呼び出した後、まず CAS が呼び出されてロックを取得します。この時点でたまたまロックが占有されていない場合は、ロックが直接取得されて返されます。
  2. CAS が失敗した後、不公平なロックは公平なロックと同様に tryAcquire メソッドに入ります。tryAcquire メソッドでは、この時点でロックが解放されていることが判明した場合 (state == 0)、不当なロックはCAS は直接ロックを取得しますが、フェアロックは待機キューに待機状態のスレッドがあるかどうかを判断し、存在する場合はロックを取得せずに後ろにキューイングされます。

Java 同時実行に関するナレッジ ポイントを要約する

# 相対的に言えば、不公平なロックはスループットが比較的大きいため、パフォーマンスが向上します。もちろん、不公平なロックにより、ロックを取得する時間がより不確実になり、ブロッキング キュー内のスレッドが長時間にわたって枯渇する可能性があります。

32.CASについてはどうですか? CASは何を知っているのでしょうか?

CAS は CompareAndSwap と呼ばれ、比較と交換を行います。主にプロセッサ命令を使用して操作のアトミック性を確保します。

CAS 命令には、共有変数のメモリ アドレス A、期待値 B、共有変数の新しい値 C の 3 つのパラメータが含まれています。

メモリのアドレス A の値が B に等しい場合にのみ、メモリのアドレス A の値を新しい値 C に更新できます。 CAS 命令自体は CPU 命令としてアトミック性を保証できます。

33.CAS の何が問題なのでしょうか?の解き方?

CAS の 3 つの古典的な問題:

Java 同時実行に関するナレッジ ポイントを要約する

ABA 問題

同時環境では、初期条件が A であると仮定すると、データを変更し、A であることが判明した場合、変更が実行されます。しかし、あなたが見ているのは A ですが、A が B に変化し、B が再び A に戻った可能性があります。この時点で、A はもう一方の A ではなくなります。データの変更が成功した場合でも、問題が発生する可能性があります。

ABA 問題を解決するにはどうすればよいですか?

  • バージョン番号を追加

変数が変更されるたびに、変数のバージョン番号に 1 が追加されます。このように、A-> だけです。 ;B->A の場合、A の値は変わっていないのに、バージョン番号が変わっているので、もう一度バージョン番号を判定すると、この時点で A が変わっていることがわかります。オプティミスティック ロックのバージョン番号を参照するこのアプローチにより、データに実際的なテストを行うことができます。

Java は AtomicStampReference クラスを提供します。その CompareAndSet メソッドは、まず現在のオブジェクト参照値が予期される参照と等しいかどうか、および現在のスタンプ (Stamp) フラグが予期されるフラグと等しいかどうかをチェックします。すべてが等しい場合, アトミックに 参照値とスタンプ値が指定された更新値に更新されます。

ループ パフォーマンス オーバーヘッド

Spin CAS がループ内で実行されても成功しない場合、CPU に非常に大きな実行オーバーヘッドが生じます。

ループ パフォーマンスのオーバーヘッド問題を解決するにはどうすればよいですか?

Java では、スピン CAS を使用する多くの場所でスピン数に制限があり、一定の数を超えるとスピンが停止します。

1 つの変数のアトミック操作のみを保証できます

CAS は、変数に対する操作のアトミック性を保証します。複数の変数が操作される場合、CAS は現在、操作のアトミック性を直接保証できません。の。

1 つの変数しか保証できないアトミック操作の問題を解決するにはどうすればよいでしょうか?

  • ロックを使用して操作の原子性を確保することを検討できます。
  • 複数の変数をマージし、複数の変数をオブジェクトにカプセル化し、AtomicReference セックスを通じて原子性を確保することを検討できます。

34.Java でアトミック性を確保する方法は何ですか?マルチスレッド下で i の結果が正しいことを確認するにはどうすればよいですか?

Java 同時実行に関するナレッジ ポイントを要約する

  • AtomicInteger などの循環アトミック クラスを使用して、i アトミック操作を実装します。
  • juc パッケージのロック (ReentrantLock など) を使用して、 i 操作を追加します。アトミック性を達成するには、lock.lock() をロックします。
  • i 操作をロックするには、synchronized を使用します。

35. アトミック操作クラスについてどれくらい知っていますか?

プログラムが変数を更新する際、複数のスレッドが同時に変数を更新すると、例えば変数i=1、スレッドAがi 1を更新し、スレッドBも更新すると予期せぬ値が得られる可能性があります。 i を 1 に更新します。 2 つ後 スレッド操作の後、i は 3 に等しくない場合がありますが、2 に等しくなります。スレッド A と B は変数 i を更新するときに両方とも 1 を取得するため、これはスレッド安全でない更新操作です。一般に、この問題を解決するには synchronized を使用します。Synchronized を使用すると、複数のスレッドが変数 i を同時に更新することがなくなります。

実際には、これに加えて、さらに軽量なオプションがあります。Java は、JDK 1.5 から java.util.concurrent.atomic パッケージを提供しています。このパッケージのアトミック操作クラスは、簡単な使用法とパフォーマンスを提供します。 - 変数を更新するための効率的でスレッドセーフな方法。

変数の種類が多いため、Atomic パッケージでは、アトミック更新基本型、アトミック更新配列、アトミック更新参照、アトミック更新リファレンスの 4 種類のアトミック更新メソッドに属する合計 13 個のクラスが提供されています。プロパティ (フィールド) のアトミック更新。

Java 同時実行に関するナレッジ ポイントを要約する

#Atomic パッケージ内のクラスは、基本的に Unsafe を使用して実装されたラッパー クラスです。

使用原子的方式更新基本类型,Atomic包提供了以下3个类:

  • AtomicBoolean:原子更新布尔类型。

  • AtomicInteger:原子更新整型。

  • AtomicLong:原子更新长整型。

通过原子的方式更新数组里的某个元素,Atomic包提供了以下4个类:

  • AtomicIntegerArray:原子更新整型数组里的元素。

  • AtomicLongArray:原子更新长整型数组里的元素。

  • AtomicReferenceArray:原子更新引用类型数组里的元素。

  • AtomicIntegerArray类主要是提供原子的方式更新数组里的整型

原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类。Atomic包提供了以下3个类:

  • AtomicReference:原子更新引用类型。

  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

如果需原子地更新某个类里的某个字段时,就需要使用原子更新字段类,Atomic包提供了以下3个类进行原子字段更新:

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的 ABA问题。

36.AtomicInteger 的原理?

一句话概括:使用CAS实现

以AtomicInteger的添加方法为例:

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

通过Unsafe类的实例来进行添加操作,来看看具体的CAS操作:

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

compareAndSwapInt 是一个native方法,基于CAS来操作int类型变量。其它的Java 同時実行に関するナレッジ ポイントを要約する基本都是大同小异。

37.线程死锁了解吗?该如何避免?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

Java 同時実行に関するナレッジ ポイントを要約する

那么为什么会产生死锁呢? 死锁的产生必须具备以下四个条件:

Java 同時実行に関するナレッジ ポイントを要約する

  • 互斥条件:指线程对己经获取到的资源进行它性使用,即该资源同时只由一个线程占用。如果此时还有其它线程请求获取获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  • 请求并持有条件:指一个 线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其它线程占有,所以当前线程会被阻塞,但阻塞 的同时并不释放自己已经获取的资源。
  • 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其它线程抢占,只有在自己使用完毕后才由自己释放该资源。
  • 环路等待条件:指在发生死锁时,必然存在一个线程——资源的环形链,即线程集合 {T0,T1,T2,…… ,Tn} 中 T0 正在等待一 T1 占用的资源,Tl1正在等待 T2用的资源,…… Tn 在等待己被 T0占用的资源。

该如何避免死锁呢?答案是至少破坏死锁发生的一个条件

  • 其中,互斥这个条件我们没有办法破坏,因为用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢?

  • 对于“请求并持有”这个条件,可以一次性请求所有的资源。

  • 对于“不可剥夺”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

  • 对于“环路等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。

38.那死锁问题怎么排查呢?

可以使用jdk自带的命令行工具排查:

  1. 使用jps查找运行的Java进程:jps -l
  2. 使用jstack查看线程堆栈信息:jstack -l 进程id

基本就可以看到死锁的信息。

还可以利用图形化工具,比如JConsole。出现线程死锁以后,点击JConsole线程面板的检测到死锁按钮,将会看到线程的死锁信息。

Java 同時実行に関するナレッジ ポイントを要約する

39.CountDownLatch(倒计数器)了解吗?

CountDownLatch,倒计数器,有两个常见的应用场景[18]:

场景1:协调子线程结束动作:等待所有子线程运行结束

CountDownLatch允许一个或多个线程等待其他线程完成操作。

例如,我们很多人喜欢玩的王者荣耀,开黑的时候,得等所有人都上线之后,才能开打。

Java 同時実行に関するナレッジ ポイントを要約する

CountDownLatch模仿这个场景(参考[18]):

创建大乔、兰陵王、安其拉、哪吒和铠等五个玩家,主线程必须在他们都完成确认后,才可以继续运行。

在这段代码中,new CountDownLatch(5)用户创建初始的latch数量,各玩家通过countDownLatch.countDown()完成状态确认,主线程通过countDownLatch.await()等待。

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(5);

        Thread 大乔 = new Thread(countDownLatch::countDown);
        Thread 兰陵王 = new Thread(countDownLatch::countDown);
        Thread 安其拉 = new Thread(countDownLatch::countDown);
        Thread 哪吒 = new Thread(countDownLatch::countDown);
        Thread 铠 = new Thread(() -> {
            try {
                // 稍等,上个卫生间,马上到...
                Thread.sleep(1500);
                countDownLatch.countDown();
            } catch (InterruptedException ignored) {}
        });

        大乔.start();
        兰陵王.start();
        安其拉.start();
        哪吒.start();
        铠.start();
        countDownLatch.await();
        System.out.println("所有玩家已经就位!");
    }

场景2. 协调子线程开始动作:统一各线程动作开始的时机

王者游戏中也有类似的场景,游戏开始时,各玩家的初始状态必须一致。不能有的玩家都出完装了,有的才降生。

所以大家得一块出生,在

Java 同時実行に関するナレッジ ポイントを要約する

在这个场景中,仍然用五个线程代表大乔、兰陵王、安其拉、哪吒和铠等五个玩家。需要注意的是,各玩家虽然都调用了start()线程,但是它们在运行时都在等待countDownLatch的信号,在信号未收到前,它们不会往下执行。

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);

        Thread 大乔 = new Thread(() -> waitToFight(countDownLatch));
        Thread 兰陵王 = new Thread(() -> waitToFight(countDownLatch));
        Thread 安其拉 = new Thread(() -> waitToFight(countDownLatch));
        Thread 哪吒 = new Thread(() -> waitToFight(countDownLatch));
        Thread 铠 = new Thread(() -> waitToFight(countDownLatch));

        大乔.start();
        兰陵王.start();
        安其拉.start();
        哪吒.start();
        铠.start();
        Thread.sleep(1000);
        countDownLatch.countDown();
        System.out.println("敌方还有5秒达到战场,全军出击!");
    }

    private static void waitToFight(CountDownLatch countDownLatch) {
        try {
            countDownLatch.await(); // 在此等待信号再继续
            System.out.println("收到,发起进攻!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

CountDownLatch的核心方法也不多:

  • await():等待latch降为0;
  • boolean await(long timeout, TimeUnit unit):等待latch降为0,但是可以设置超时时间。比如有玩家超时未确认,那就重新匹配,总不能为了某个玩家等到天荒地老。
  • countDown():latch数量减1;
  • getCount():获取当前的latch数量。

40.CyclicBarrier(同步屏障)了解吗?

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

它和CountDownLatch类似,都可以协调多线程的结束动作,在它们结束后都可以执行特定动作,但是为什么要有CyclicBarrier,自然是它有和CountDownLatch不同的地方。

不知道你听没听过一个新人UP主小约翰可汗,小约翰生平有两大恨——“Java 同時実行に関するナレッジ ポイントを要約する”我们来还原一下事情的经过:小约翰在亲政后认识了新垣结衣,于是决定第一次选妃,向结衣表白,等待回应。然而新垣结衣回应嫁给了星野源,小约翰伤心欲绝,发誓生平不娶,突然发现了铃木爱理,于是小约翰决定第二次选妃,求爱理搭理,等待回应。

Java 同時実行に関するナレッジ ポイントを要約する

我们拿代码模拟这一场景,发现CountDownLatch无能为力了,因为CountDownLatch的使用是一次性的,无法重复利用,而这里等待了两次。此时,我们用CyclicBarrier就可以实现,因为它可以重复利用。

Java 同時実行に関するナレッジ ポイントを要約する

Java 同時実行に関するナレッジ ポイントを要約する:

Java 同時実行に関するナレッジ ポイントを要約する

CyclicBarrier最最核心的方法,仍然是await():

  • 如果当前线程不是第一个到达屏障的话,它将会进入等待,直到其他线程都到达,除非发生被中断屏障被拆除屏障被重设等情况;

上面的例子抽象一下,本质上它的流程就是这样就是这样:

Java 同時実行に関するナレッジ ポイントを要約する

41.CyclicBarrier和CountDownLatch有什么区别?

两者最核心的区别[18]:

  • CountDownLatch は 1 回限りの使用ですが、CyclicBarrier はバリアを複数回設定して再利用できます。
  • CountDownLatch の各サブスレッドは他のスレッドを待つことができず、独自のタスクのみを完了できます。 CyclicBarrier の各スレッドは他のスレッドを待機できます

それらの違いは表にまとめられています:

##CyclicBarrier では、スレッドが割り込みやタイムアウトなどに遭遇すると、スレッドは待機状態になります。問題が発生します CountDownLatch では、1 つのスレッドで問題が発生しても、他のスレッドは影響を受けません

42.Semaphore(信号量)了解吗?

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

听起来似乎很抽象,现在汽车多了,开车出门在外的一个老大难问题就是停车 。停车场的车位是有限的,只能允许若干车辆停泊,如果停车场还有空位,那么显示牌显示的就是绿灯和剩余的车位,车辆就可以驶入;如果停车场没位了,那么显示牌显示的就是绿灯和数字0,车辆就得等待。如果满了的停车场有车离开,那么显示牌就又变绿,显示空车位数量,等待的车辆就能进停车场。

Java 同時実行に関するナレッジ ポイントを要約する

我们把这个例子类比一下,车辆就是线程,进入停车场就是线程在执行,离开停车场就是线程执行完毕,看见红灯就表示线程被阻塞,不能执行,Semaphore的本质就是协调多个线程对共享资源的获取

Java 同時実行に関するナレッジ ポイントを要約する

我们再来看一个Semaphore的用途:它可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。

假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制,如下:

public class SemaphoreTest {
    private static final int THREAD_COUNT = 30;
    private static ExecutorService threadPool = Executors.newJava 同時実行に関するナレッジ ポイントを要約する(THREAD_COUNT);
    private static Semaphore s = new Semaphore(10);

    public static void main(String[] args) {
        for (int i = 0; i <p>在代码中,虽然有30个线程在执行,但是只允许10个并发执行。Semaphore的构造方法<code>Semaphore(int permits</code>)接受一个整型的数字,表示可用的许可证数量。<code>Semaphore(10)</code>表示允许10个线程获取许可证,也就是最大并发数是10。Semaphore的用法也很简单,首先线程使用 Semaphore的acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。</p><h2>43.Exchanger 了解吗?</h2><p>Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。</p><p><img src="https://img.php.cn/upload/article/000/000/067/a99f1f171b5f8c414e2981e6a7d5189a-57.png" alt="Java 同時実行に関するナレッジ ポイントを要約する"></p><p>这两个线程通过 exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。</p><p>Exchanger可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2个交配结果。Exchanger也可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel之后,系统需要加载这两个Excel,并对两个Excel数据进行校对,看看是否录入一致。</p><pre class="brush:php;toolbar:false">public class ExchangerTest {
    private static final Exchanger<string> exgr = new Exchanger<string>();
    private static ExecutorService threadPool = Executors.newJava 同時実行に関するナレッジ ポイントを要約する(2);

    public static void main(String[] args) {
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String A = "银行流水A"; // A录入银行流水数据 
                    exgr.exchange(A);
                } catch (InterruptedException e) {
                }
            }
        });
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    String B = "银行流水B"; // B录入银行流水数据 
                    String A = exgr.exchange("B");
                    System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:"
                            + A + ",B录入是:" + B);
                } catch (InterruptedException e) {
                }
            }
        });
        threadPool.shutdown();
    }}</string></string>

假如两个线程有一个没有执行exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x, long timeOut, TimeUnit unit)设置最大等待时长

44.什么是线程池?

线程池: 简单理解,它就是一个Java 同時実行に関するナレッジ ポイントを要約する。

Java 同時実行に関するナレッジ ポイントを要約する

  • 它帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象,创建一个对象,需要经过类加载过程,销毁一个对象,需要走GC垃圾回收流程,都是需要资源开销的。
  • 提高响应速度。 如果任务到达了,相对于从线程池拿线程,重新去创建一条线程执行,速度肯定慢很多。
  • 重复利用。 线程用完,再放回池子,可以达到重复利用的效果,节省资源。

45.能说说工作中线程池的应用吗?

之前我们有一个和第三方对接的需求,需要向第三方推送数据,引入了多线程来提升数据推送的效率,其中用到了线程池来管理线程。

Java 同時実行に関するナレッジ ポイントを要約する

Java 同時実行に関するナレッジ ポイントを要約する如下:

Java 同時実行に関するナレッジ ポイントを要約する

完整可运行代码地址:https://gitee.com/fighter3/thread-demo.git

线程池的参数如下:

  • corePoolSize:线程核心参数选择了CPU数×2

  • maximumPoolSize:最大线程数选择了和核心线程数相同

  • keepAliveTime:非核心闲置线程存活时间直接置为0

  • unit:非核心线程保持存活的时间选择了 TimeUnit.SECONDS 秒

  • workQueue:线程池等待队列,使用 LinkedBlockingQueue阻塞队列

同时还用了synchronized 来加锁,保证数据不会被重复推送:

  synchronized (PushProcessServiceImpl.class) {}

ps:这个例子只是简单地进行了数据推送,实际上还可以结合其他的业务,像什么数据清洗啊、数据统计啊,都可以套用。

46.能简单说一下线程池的工作流程吗?

用一个通俗的比喻:

有一个营业厅,总共有六个窗口,现在开放了三个窗口,现在有三个窗口坐着三个营业员小姐姐在营业。

老三去办业务,可能会遇到什么情况呢?

  1. 老三发现有空间的在营业的窗口,直接去找小姐姐办理业务。

Java 同時実行に関するナレッジ ポイントを要約する

  1. 老三发现没有空闲的窗口,就在排队区排队等。

Java 同時実行に関するナレッジ ポイントを要約する

  1. 老三发现没有空闲的窗口,等待区也满了,蚌埠住了,经理一看,就让休息的小姐姐赶紧回来上班,等待区号靠前的赶紧去新窗口办,老三去排队区排队。小姐姐比较辛苦,假如一段时间发现他们可以不用接着营业,经理就让她们接着休息。

Java 同時実行に関するナレッジ ポイントを要約する

  1. 老三一看,六个窗口都满了,等待区也没位置了。老三急了,要闹,经理赶紧出来了,经理该怎么办呢?

Java 同時実行に関するナレッジ ポイントを要約する

  1. 我们银行系统已经瘫痪

  2. 谁叫你来办的你找谁去

  3. 看你比较急,去队里加个塞

  4. 今天没办法,不行你看改一天

上面的这个流程几乎就跟 JDK 线程池的大致流程类似,

  1. 营业中的 3个窗口对应核心线程池数:corePoolSize
  2. 总的营业窗口数6对应:maximumPoolSize
  3. 打开的临时窗口在多少时间内无人办理则关闭对应:unit
  4. 排队区就是等待队列:workQueue
  5. 无法办理的时候银行给出的解决方法对应:RejectedExecutionHandler
  6. threadFactory 该参数在 JDK 中是 线程工厂,用来创建线程对象,一般不会动。

所以我们线程池的工作流程也比较好理解了:

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
  • 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
  • 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
  • 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
  • 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会根据拒绝策略来对应处理。

Java 同時実行に関するナレッジ ポイントを要約する

  1. 当一个线程完成任务时,它会从队列中取下一个任务来执行。

  2. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

47.线程池主要参数有哪些?

Java 同時実行に関するナレッジ ポイントを要約する

线程池有七大参数,需要重点关注corePoolSizemaximumPoolSizeworkQueuehandler这四个。

  1. corePoolSize

此值是用来初始化线程池中核心线程数,当线程池中线程池数corePoolSize时,系统默认是添加一个任务才创建一个线程池。当线程数 = corePoolSize时,新任务会追加到workQueue中。

  1. maximumPoolSize

maximumPoolSize表示允许的最大线程数 = (非核心线程数+核心线程数),当BlockingQueue也满了,但线程池中总线程数 maximumPoolSize时候就会再次创建新的线程。

  1. keepAliveTime

非核心线程 =(maximumPoolSize - corePoolSize ) ,非核心线程闲置下来不干活最多存活时间。

  1. unit

线程池中非核心线程保持存活的时间的单位

  • TimeUnit.DAYS; 天
  • TimeUnit.HOURS; 小时
  • TimeUnit.MINUTES; 分钟
  • TimeUnit.SECONDS; 秒
  • TimeUnit.MILLISECONDS; 毫秒
  • TimeUnit.MICROSECONDS; 微秒
  • TimeUnit.NANOSECONDS; 纳秒
  1. workQueue

线程池等待队列,维护着等待执行的Runnable对象。当运行当线程数= corePoolSize时,新的任务会被添加到workQueue中,如果workQueue也满了则尝试用非核心线程执行任务,等待队列应该尽量用有界的。

  1. threadFactory

创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等。

  1. handler

corePoolSizeworkQueuemaximumPoolSize都不可用的时候执行的饱和策略。

48.线程池的拒绝策略有哪些?

类比前面的例子,无法办理业务时的处理方式,帮助记忆:

Java 同時実行に関するナレッジ ポイントを要約する

  • AbortPolicy :直接抛出异常,默认使用此策略
  • CallerRunsPolicy:用调用者所在的线程来执行任务
  • DiscardOldestPolicy:丢弃阻塞队列里最老的任务,也就是队列里靠前的任务
  • DiscardPolicy :当前任务直接丢弃

想实现自己的拒绝策略,实现RejectedExecutionHandler接口即可。

49.线程池有哪几种工作队列?

常用的阻塞队列主要有以下几种:

Java 同時実行に関するナレッジ ポイントを要約する

  • ArrayBlockingQueue:ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。
  • LinkedBlockingQueue:LinkedBlockingQueue(可设置容量队列)是基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene;newJava 同時実行に関するナレッジ ポイントを要約する线程池使用了这个队列
  • DelayQueue:DelayQueue(延迟队列)是一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。newScheduledThreadPool线程池使用了这个队列。
  • PriorityBlockingQueue:PriorityBlockingQueue(优先级队列)是具有优先级的无界阻塞队列
  • SynchronousQueue:SynchronousQueue(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene,newCachedThreadPool线程池使用了这个队列。

50.线程池提交execute和submit有什么区别?

  1. execute 用于提交不需要返回值的任务
threadsPool.execute(new Runnable() { 
    @Override public void run() { 
        // TODO Auto-generated method stub } 
    });
  1. submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个 future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值
Future<object> future = executor.submit(harReturnValuetask); try { Object s = future.get(); } catch (InterruptedException e) { 
    // 处理中断异常 } catch (ExecutionException e) { 
    // 处理无法执行任务异常 } finally { 
    // 关闭线程池 executor.shutdown();}</object>

51.线程池怎么关闭知道吗?

可以通过调用线程池的shutdownshutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

shutdown() 将线程池状态置为shutdown,并不会立即停止

  1. 停止接收外部submit的任务
  2. 内部正在跑的任务和队列里等待的任务,会执行完
  3. 等到第二步完成后,才真正停止

shutdownNow() 将线程池状态置为stop。一般会立即停止,事实上不一定

  1. 和shutdown()一样,先停止接收外部提交的任务
  2. 忽略队列里等待的任务
  3. 尝试将正在跑的任务interrupt中断
  4. 返回未执行的任务列表

shutdown 和shutdownnow简单来说区别如下:

  • shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停下了。这样做立即生效,但是风险也比较大。
  • shutdown()只是关闭了提交通道,用submit()是无效的;而内部的任务该怎么跑还是怎么跑,跑完再彻底停止线程池。

52.线程池的线程数应该怎么配置?

线程在Java中属于稀缺资源,线程池不是越大越好也不是越小越好。任务分为计算密集型、IO密集型、混合型。

  1. 计算密集型:大部分都在用CPU跟内存,加密,逻辑操作业务处理等。
  2. IO密集型:数据库链接,网络通讯传输等。

常见Java 同時実行に関するナレッジ ポイントを要約する配置方案-来源美团技术博客

一般的经验,不同类型线程池的参数配置:

  1. 计算密集型一般推荐线程池不要过大,一般是CPU数 + 1,+1是因为可能存在页缺失(就是可能存在有些数据在硬盘中需要多来一个线程将数据读入内存)。如果线程池数太大,可能会频繁的 进行线程上下文切换跟任务调度。获得当前CPU核心数代码如下:
Runtime.getRuntime().availableProcessors();
  1. IO密集型:线程数适当大一点,机器的Cpu核心数*2。
  2. 混合型:可以考虑根绝情况将它拆分成CPU密集型和IO密集型任务,如果执行时间相差不大,拆分可以提升吞吐量,反之没有必要。

当然,实际应用中没有固定的公式,需要结合测试和监控来进行调整。

53.有哪几种常见的线程池?

面试常问,主要有四种,都是通过工具类Excutors创建出来的,需要注意,阿里巴巴《Java开发手册》里禁止使用这种方式来创建线程池。

Java 同時実行に関するナレッジ ポイントを要約する

  • newJava 同時実行に関するナレッジ ポイントを要約する (固定数目线程的线程池)

  • newCachedThreadPool (可缓存线程的线程池)

  • newSingleThreadExecutor (单线程的线程池)

  • newScheduledThreadPool (定时及周期执行的线程池)

54.能说一下四种常见线程池的原理吗?

前三种线程池的构造直接调用ThreadPoolExecutor的构造方法。

newSingleThreadExecutor

  public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<runnable>(),
                                    threadFactory));
    }</runnable>

线程池特点

  • 核心线程数为1
  • 最大线程数也为1
  • 阻塞队列是无界队列LinkedBlockingQueue,可能会导致OOM
  • keepAliveTime为0

Java 同時実行に関するナレッジ ポイントを要約する

工作流程:

  • 提交任务
  • 线程池是否有一条线程在,如果没有,新建线程执行任务
  • 如果有,将任务加到阻塞队列
  • 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个线程执行任务。

适用场景

适用于串行执行任务的场景,一个任务一个任务地执行。

newJava 同時実行に関するナレッジ ポイントを要約する

  public static ExecutorService newJava 同時実行に関するナレッジ ポイントを要約する(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<runnable>(),
                                      threadFactory);
    }</runnable>

线程池特点:

  • 核心线程数和最大线程数大小一样
  • 没有所谓的非空闲时间,即keepAliveTime为0
  • 阻塞队列为无界队列LinkedBlockingQueue,可能会导致OOM

Java 同時実行に関するナレッジ ポイントを要約する

工作流程:

  • 提交任务
  • 如果线程数少于核心线程,创建核心线程执行任务
  • 如果线程数等于核心线程,把任务添加到LinkedBlockingQueue阻塞队列
  • 如果线程执行完任务,去阻塞队列取任务,继续执行。

使用场景

Java 同時実行に関するナレッジ ポイントを要約する 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。

newCachedThreadPool

   public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<runnable>(),
                                      threadFactory);
    }</runnable>

线程池特点:

  • 核心线程数为0
  • 最大线程数为Integer.MAX_VALUE,即无限大,可能会因为无限创建线程,导致OOM
  • 阻塞队列是SynchronousQueue
  • 非核心线程空闲存活时间为60秒

当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。

Java 同時実行に関するナレッジ ポイントを要約する

工作流程:

  • 提交任务
  • 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
  • 判断是否有空闲线程,如果有,就去取出任务执行。
  • 如果没有空闲线程,就新建一个线程执行。
  • 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。

适用场景

用于并发执行大量短期的小任务。

newScheduledThreadPool

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

线程池特点

  • 最大线程数为Integer.MAX_VALUE,也有OOM的风险
  • 阻塞队列是DelayedWorkQueue
  • keepAliveTime为0
  • scheduleAtFixedRate() :按某种速率周期执行
  • scheduleWithFixedDelay():在某个延迟后执行

Java 同時実行に関するナレッジ ポイントを要約する

工作机制

  • 线程从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前时间。
  • 线程执行这个ScheduledFutureTask。
  • 线程修改ScheduledFutureTask的time变量为下次将要被执行的时间。
  • 线程把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

Java 同時実行に関するナレッジ ポイントを要約する

使用场景

周期性执行任务的场景,需要限制线程数量的场景

使用无界队列的线程池会导致什么问题吗?

例如newJava 同時実行に関するナレッジ ポイントを要約する使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM。

55.线程池异常怎么处理知道吗?

在使用线程池处理任务的时候,任务代码可能抛出RuntimeException,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。

常见的异常处理方式:

Java 同時実行に関するナレッジ ポイントを要約する

56.能说一下线程池有几种状态吗?

线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。

   //线程池状态
   private static final int RUNNING    = -1 <p>线程池各个状态切换图:</p><p><img src="https://img.php.cn/upload/article/000/000/067/47690a2c7799a7bd0d10ed5490da3a7b-77.png" alt="Java 同時実行に関するナレッジ ポイントを要約する"></p><p><strong>RUNNING</strong></p>
  • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
  • 调用线程池的shutdownNow()方法,可以切换到STOP状态;

SHUTDOWN

  • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

STOP

  • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  • 线程池中执行的任务为空,进入TIDYING状态;

TIDYING

  • 该状态表明所有的任务已经运行终止,记录的任务数量为0。
  • terminated()执行完毕,进入TERMINATED状态

TERMINATED

  • 该状态表示线程池彻底终止

57.线程池如何实现参数的动态修改?

线程池提供了几个 setter方法来设置线程池的参数。

JDK Java 同時実行に関するナレッジ ポイントを要約する设置接口来源参考[7]

这里主要有两个思路:

动态修改Java 同時実行に関するナレッジ ポイントを要約する

  • #マイクロサービス アーキテクチャでは、Nacos、Apollo などの構成センターを使用することも、独自の構成センターを開発することもできます。ビジネス サービスは、スレッド プール構成を読み取り、対応するスレッド プール インスタンスを取得して、スレッド プール パラメータを変更します。

  • 構成センターの使用が制限されている場合は、ThreadPoolExecutor を自分で拡張し、メソッドを書き換え、スレッド プール パラメーターの変更を監視し、スレッド プールを動的に変更することもできます。パラメーター。

スレッド プールのチューニングについてご存知ですか?

スレッド プールの構成には決まった計算式はありません。通常、スレッド プールは事前にある程度評価されます。一般的な評価スキームは次のとおりです。

オンラインにする前にもオンラインにした後、完全なスレッド プール監視メカニズムを完全にテストして確立します。

线程池评估方案 来源参考[7] プロセス中に監視およびアラーム メカニズムを組み合わせてスレッド プールの問題を分析するか、ポイントを最適化し、スレッド プールの動的パラメーター構成メカニズムと組み合わせて構成を調整します。

その後は注意深く観察し、随時調整してください。

#具体的なチューニングのケースについては、[7] Meituan Technology Blog を参照してください。

Java 同時実行に関するナレッジ ポイントを要約する58. スレッド プールを設計して実装できますか?

この質問は Alibaba のインタビューで頻繁に登場します

スレッド プールの実装原理を確認できます。以前誰かがスレッド プールについてこのように話していたなら、私はとっくに理解しているはずです。もちろん、私たちはそれを自分たちで実装します。スレッドプールのコアプロセスを把握するだけで十分です - 参考文献 [6]:

私たち自身の実装は、このコアを完成させることですプロセス:

Java 同時実行に関するナレッジ ポイントを要約する

スレッド プールには N 個のワーカー スレッドがあります

実行のためにタスクをスレッド プールに送信します
  • スレッド プールがいっぱいの場合は、タスクをキューに追加します。
  • 最後に、時間が空いたら、タスクをキューに入れて実行します。
  • 実装コード [6]:

このようにして、メインスレッドプールが実現され、プロセスクラスが完成します。

Java 同時実行に関するナレッジ ポイントを要約する59. 単一マシンのスレッド プールの実行がパワーオフされている場合はどうすればよいですか?

キュー内で処理されてブロックされているタスクのトランザクション管理、またはブロックされたキュー内のタスクの永続化を行うことができます。また、停電やシステムクラッシュが発生して操作を続行できない場合、トレースバック ログ 正常に実行された

processing

操作を元に戻すメソッド。次に、ブロッキング キュー全体を再実行します。

言い換えると、ブロッキング キューは永続化され、タスクのトランザクション制御が処理され、停電後にタスクのロールバックが処理され、ログを通じて操作が復元され、ブロッキング キュー内のデータはキューはサーバーの再起動後に再ロードされます。 同時コンテナとフレームワーク

一部の同時コンテナについては、

CopyOnWriteList

ConcurrentHashMap## を含む「Counter Attack: Java Collection of Thirty question」をチェックしてください。 #これら 2 つのスレッドセーフなコンテナ クラスに関する質問と回答。 。

60.Fork/Join フレームワークを理解していますか? Fork/Joinフレームワークは、Java7で提供されているタスクを並列実行するためのフレームワークで、大きなタスクをいくつかの小さなタスクに分割し、最後にそれぞれの小さなタスクの結果をまとめて結果を得るフレームワークです。大きな仕事。

Fork/Join フレームワークをマスターするには、まず

Divide and Conquer

Work Stealing Algorithm

の 2 つのポイントを理解する必要があります。

分割統治

フォーク/結合フレームワークの定義は、実際には分割統治のアイデアを具体化しています。つまり、サイズ N の問題を小さい K 個の問題に分解します。これらの副問題は互いに独立しており、元の問題と同じ性質を持っています。副問題の解決策を見つけることで、元の問題の解決策を得ることができます。

作業盗用アルゴリズム

Java 同時実行に関するナレッジ ポイントを要約する

大きなタスクをいくつかの小さなタスクに分割し、これらの小さなタスクを異なるキューに入れると、それぞれがキュー内のタスクを実行するための別のスレッド。

その後、問題が発生します。一部のスレッドは激しく動作し、一部のスレッドはゆっくりと動作します。作業を終了したスレッドをアイドル状態のままにすることはできません。作業を終了していないスレッドの作業を許可する必要があります。他のスレッドのキューからタスクを盗んで実行する、いわゆる ワークスチール

です。

作業盗用が発生すると、同じキューにアクセスします。盗むタスク スレッドと盗まれたタスク スレッド間の競合を減らすために、通常、タスクは両端キューを使用し、盗まれたタスク スレッドは、タスクはキューの先頭から取得され、タスクを盗むスレッドは常に両端のキューの末尾からタスクを取得して実行されます。

Fork/Join フレームワーク アプリケーションの例を見て、1 ~ n の間の合計を計算します: 1 2 3 … n

  • 设置一个分割阈值,任务大于阈值就拆分任务
  • 任务有结果,所以需要继承RecursiveTask
public class CountTask extends RecursiveTask<integer> {
    private static final int THRESHOLD = 16; // 阈值
    private int start;
    private int end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // 如果任务足够小就计算任务
        boolean canCompute = (end - start)  result = forkJoinPool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }
    }
    }</integer>

ForkJoinTask与一般Task的主要区别在于它需要实现compute方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务。如果比较大,就必须分割成两个子任务,每个子任务在调用fork方法时,又会进compute方法,看看当前子任务是否需要继续分割成子任务,如果不需要继续分割,则执行当前子任务并返回结果。使用join方法会等待子任务执行完并得到其结果。

推荐学习:《java教程

CyclicBarrier CountDownLatch
CyclicBarrier は再利用可能であり、その中のスレッドはすべてのスレッドがタスクを完了するまで待機します。その際、バリアは解除され、特定のアクションを選択的に実行できるようになります。 CountDownLatch は 1 回限りで、カウンタが 0 になるまで異なるスレッドが同じカウンタ上で動作します。
CyclicBarrier はスレッドの数を対象とします CountDownLatch はタスクの数を重視します
CyclicBarrier を使用する場合は、構築でコラボレーションに参加するスレッドの数を指定する必要があり、これらのスレッドは await() メソッドを呼び出す必要があります。 CountDownLatch を使用するときは、タスクの数を指定する必要があります。どのスレッドがこれらのタスクを完了するかは関係ありません。
CyclicBarrier は、すべてのスレッドが解放された後に再利用できます CountDownLatch カウンタが 0 になると使用できなくなります。

以上がJava 同時実行に関するナレッジ ポイントを要約するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。