Javaのスレッドを深く理解する


Javaのスレッドを深く理解する
スレッドはタスクを実行するために使用でき、タスクの実行は非同期であり、後続のコードをブロックしないことを知っています.Javaプロセスでは、mainメソッドを含むクラスもスレッドで実行されます.実際のアプリケーションでは、比較的時間のかかる操作を処理する必要がある場合、プログラム全体の応答に影響を与えないように、通常、この時間のかかる操作をスレッドにカプセル化し、非同期で実行します.しかし,スレッドはどのようにしてタスクの非同期実行を実現するのか.この文書では、スレッド実行の秘密を得るためにThreadクラスを深く理解します.
「JAVA仮想マシンを深く理解する」のスレッドに関する章によると、javaの1つのThreadがオペレーティングシステムの1つのスレッドに対応していることがわかります.オペレーティングシステムのスレッドは希少なリソースであり、スレッドを無制限に作成することはできません.これは、スレッドプールを使用する理由の1つです.
Javaでスレッドを実装するには、次の2つの方法があります.
  • はThreadクラス
  • を継承する.
  • Runnableインタフェース
  • を実装
    いずれにしても、最後のスレッドの実行はThreadのstart()メソッドを呼び出すことによって行われます.
    Threadクラスの重要な属性と方法を見てみましょう.
    // target       Thread  Thread   Runnable  
    /* What will be run. */
    private Runnable target;
    
    /* The group of this thread */
    private ThreadGroup group;
    
    //    ,     Thread     ,          ,       ,                  ,        
    /* Make sure registerNatives is the first thing  does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }
    //          :
    public static native Thread currentThread();
    public static nativevoid yield();
    public static native void sleep(long millis) throws InterruptedException;
    private native void start0();
    private native boolean isInterrupted(boolean ClearInterrupted);
    public final native boolean isAlive();
    public static native boolean holdsLock(Object obj);
    private native static StackTraceElement[][] dumpThreads(Thread[] threads);
    private native static Thread[] getThreads();
    private native void setPriority0(int newPriority);
    private native void stop0(Object o);
    private native void suspend0();
    private native void resume0();
    private native void interrupt0();
    private native void setNativeName(String name);

    次のコードが出力される内容を見てみましょう.
    public static class MyThread extends Thread{
        @Override
        public void run(){
            System.out.println("MyThread---1");
        }
    }
    public static class MyRunnable implements Runnable{
        @Override
        public void run() {
            System.out.println("MyRunnable---1");
        }
    }
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        Thread t2 = new Thread(new MyRunnable());
        t1.start();
        t2.start();
        System.out.println("MyThread---2");
        System.out.println("MyRunnable---2");
    }

    このコードの出力内容は不確定で、次のように出力される可能性があります.
    MyThread---2
    MyRunnable---2
    MyRunnable---1
    MyThread---1

    次のように出力することもできます.
    MyThread---1
    MyRunnable---1
    MyThread---2
    MyRunnable---2

    ただし、上記のコードt 1をstart(),t2.start()を次のように変更します.
    t1.run();
    t2.run();

    出力は決定されます.
    MyThread---1
    MyRunnable---1
    MyThread---2
    MyRunnable---2

    なぜstart()を使うのか、出力の内容は不確定でrun()を使うのは確定ですか?これはThreadの起動プロセスから理解する必要があります.Threadクラスのstart()メソッドのソースコードは次のとおりです.
    public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
    
        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }
    private native void start0();

    start()メソッドの内部では、nativeメソッドstart 0()が呼び出されていることがわかります.一方、Threadクラスの初期化では、ローカルメソッドを登録するregisternatives()メソッドが実行され、start 0メソッドが実際にJVM_に関連付けられているStartThreadメソッド:
    {"start0", "()V",(void *)&JVM_StartThread}

    jvm.cppには、次のコードセグメントがあります.
    JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)){
        ...
        native_thread = new JavaThread(&thread_entry, sz);
        ...
    }

    ここでJVMENTTRYは、JVMStatThread関数を定義するマクロであり、実際のプラットフォームに関連するローカルスレッドが作成されていることがわかります.スレッド関数はthread_です.entry、以下の通りです.
    static void thread_entry(JavaThread* thread, TRAPS) {
        HandleMark hm(THREAD);
        Handle obj(THREAD, thread->threadObj());
        JavaValue result(T_VOID);
        JavaCalls::call_virtual(&result,obj,KlassHandle(THREAD,SystemDictionary::Thread_klass()),
        vmSymbolHandles::run_method_name(),    //   run_method_name
        vmSymbolHandles::void_method_signature(),THREAD);
    }

    vmSymbolHandles::runmethodnameメソッドが呼び出され、runmethodnameはvmSymbols.hppはマクロで定義されています.
    class vmSymbolHandles: AllStatic {
        ...
        //               “run”
        template(run_method_name,"run")
        ...
    }

    以上のコードから分かるように、Threadがstart()メソッドを実行すると、まず新しいオペレーティングシステムのスレッドが作成され、その後、そのオペレーティングスレッドがCPUタイムスライスを取得すると、run()というコールバックメソッドが実行され、start()によって新しいスレッドが作成され、非同期にスレッドボディが実行されることが証明される.run()によってThreadオブジェクトを1つ実行する一般的な方法にすぎず,並列実行ではなくシリアル実行である.
    以下に、Threadに関する一般的な質問を添付します.
    Threadのsleep、join、yield
  • 1.sleep
  • sleep()現在のスレッドを停滞状態(現在のスレッドをブロック)にする、CPUの使用を一定時間残して他のスレッドに
  • を実行させる.
  • sleepスリープ時にオブジェクトのロックが解放されない
  • 2.joinが1つのスレッドAでスレッドBのjoinメソッドを実行すると、Aは保留し、Bの実行が完了してから後続タスク
  • を実行する.
    public static void main(String[] args){
        Thread t1 = new Thread();
        t1.start();
        t1.join();
        //       t1       
        System.out.println("t1 finished");
    }
  • 3.yield
  • yieldは終了と一時停止を意味するわけではありません.ただ、スレッドスケジューリングに誰かが必要なら、先に持って行ってもいいと言ってください.私は後で実行します.誰も必要としません.私は
  • を実行し続けます.
  • yieldが呼び出されたとき、ロックは解放されなかった
  • objectのwait、notify、notifyAll
  • 1.wait
  • wait()メソッドはObjectクラスのメソッドです.1つのスレッドがwait()メソッドに実行されると
  • スレッドは、オブジェクトに関連する待機プールに入るとともに、オブジェクトのロックが失われ、起動時に再びロック
  • が取得する.
  • wait()notify()またはnotifyAll()を使用するか、現在の待機プールのスレッド
  • を起動するために睡眠時間を指定します.
  • wait()はsynchronizedブロックに配置する必要があります.そうしないと、「java.lang.I llegalMonitorStateException」
  • とエラーが発生します.
  • 2.notify

  • wait()とnotify()は、同じ「オブジェクトモニタ」を操作する必要があります.
    Runnable1 implements Runnable{
        public void run(){
            synchronized(lock){
                //          
                lock.wait();
                System.out.println("Runnable1 has been notified by other thread");
            }
        }
    }
    Runnable2 implements Runnable{
        public void run(){
            synchronized(lock){
                System.out.println("Runnable2 will notify other thread who wait for lock");
                //                   lock.notify();
            }
        }
    }
    public static void main(String[] args){
        Object lock = new Object();
        Thread t1 = new Thread(new Runnable1(lock));
        Thread t2 = new Thread(new Runnable2(lock));
        t1.start();
        t2.start();
    }

    ThreadとRunnableの違い
  • Runnableは、Threadのstart(実際にtargetを呼び出すrunメソッド)によって起動することができ、Threadのtargetの属性はRunnable
  • である.
  • Runnableは属性資源共有を実現でき、Threadは資源共有を実現できない
  • MyRunnable r = new MyRunnable();
    Thread t1 = new Thread(r);
    Thread t2 = new Thread(r);
    // t1/t2            r,  r             
    t1.start();
    t2.start();

    スレッド間の通信方法
  • join:1つのスレッドが別のスレッドの実行が完了するのを待ってから
  • を実行します.
  • wait/notify:あるスレッドは、別のスレッドが自分の所有するオブジェクトモニタを起動するのを待ってから
  • を実行します.
  • CountdownLatch:1つのスレッド待ち(countDownLatch.await()他の任意の数のスレッド実行が完了した後(countDownLatch.contDown())
  • を実行する.
  • CyclicBarrier:すべてのスレッドがそれぞれ準備され、すべてのスレッドが準備完了(すべてcyclicBarrier.await()が呼び出された)後、後続の
  • の実行が統一的に開始される.
  • Semaphore:同時にアクセスできるスレッドの数を制御し、acquire()でライセンスを取得し、なければ待機し、release()はライセンス
  • を解放する.
  • Callable:サブスレッドは実行結果を親スレッド
  • に返す.
    FutureTask futureTask = new FutureTask<>(callable);
    new Thread(futureTask).start();
    Object result = futureTask.get();

    1)CountDownLatchとCyclicBarrierはいずれもスレッド間の待機を実現できるが、それらの側面が異なる:2)CountDownLatchは一般的にあるスレッドAがいくつかの他のスレッドがタスクを実行するのを待ってから実行するために使用される.3)CyclicBarrierは、一般的に、ある状態までスレッドのセットが互いに待機し、その後、このスレッドのセットが同時に実行される.4)また、CountDownLatchは再利用できないが、CyclicBarrierは再利用できる.5)Semaphoreは、通常、リソースのグループへのアクセスを制御するために使用されるロックと似ています.
    スレッドプールの原理
    スレッドプールには、コアスレッド数coreNumと最大スレッド数maxNumの2つのパラメータがあります.
    スレッドプールを初期化すると、コアスレッド数は5、最大スレッド数は10、スレッドプールを初期化すると、中にはスレッドがありません.
    1つのタスクが来たときにスレッドが初期化され、もう1つのタスクが来たらスレッドが初期化され、5つのスレッドが連続的に初期化された後、6番目のタスクが来たら
    6番目のタスクがブロックされたキューに配置されます
    スレッドプールに5つのスレッドがあり、そのうちの1つが空いている場合は、ブロックキューから6番目のタスクを取得して実行します.
    スレッドプールの5つのスレッドがrunning状態の場合、タスクはブロックキューに保存されます.
    キューがいっぱいで、最大スレッド数が10に設定されていますが、スレッドプールには5つのスレッドしかありません.この場合、ブロックキューに保存できないタスクを実行するスレッドが新規作成されます.スレッドプールには6つのスレッドがあります.
    スレッドプールのスレッド数が10個に達し、ブロックキューもいっぱいになった場合は、カスタムreject関数でこれらのタスクを処理できます.
    最後にしばらく実行すると、ブロックキュー内のタスクも実行され、スレッドプール内のコアスレッド数を超えるスレッドは空き時間内に自動的に回収されます.