Android 同時プログラミングのスレッド原理

1. プロセスとスレッドの概念

そういった公式の概念はさておき、大まかに次のように理解できます: プロセスとは携帯電話上で実行されるアプリケーションであり、それらはすべて 1 つずつのプロセスです (もちろん、一部のアプリはマルチプロセスです。これについては最初に説明しません)。 。スレッドは、プロセス内の対応するタスクの実行制御フローです。プロセスを工場にたとえると、工場内の各生産ラインは糸とみなすことができます。

高速バッファの概念:

初期の頃、コンピューターにはプロセスしかありませんでした。同時に、プロセスはメモリ上で動作します。しかし、これでは効率が悪くなります。たとえば、ファイルの読み取りおよび書き込み操作を処理したい場合、他の作業を行う前に操作が完了するまで待つ必要があります。並行性の概念が登場すると、複数のプロセスを順番に切り替えて、相対的な意味での「同時」メモリ操作を実現できるようになります。

しかし、この方法ではプロセス間の切り替えに多くのリソースが必要になるという問題があります。したがって、より効率化するためにスレッドが設計され、メモリ上のデータは高速バッファに格納され、各スレッドには対応する高速バッファがあり、スレッドは高速バッファ内のデータにアクセスします。これにより、元のプロセス間スイッチングがスレッド間のスイッチングになり、より軽量になり、リソースの消費が削減されます。

各スレッドには独自の高速バッファ (レジスタ) があります。CPU 内のメモリとして理解できます。コアはスレッドを表すため、4 コアの 8G 携帯電話を想定すると、2 コアは 2G になります。

私たちが通常作成する Java ファイルは、javac を介して .java ファイルを .class ファイルに変換し、クラス ローダー、classLoader を介して .class ファイルをメモリにロードし、jvm がこの .class バイトコード ファイルを実行することを知っています。一部のメモリ分割が実行されます。

ここには主に 2 つの区分があり、1 つはスレッド共有領域、もう 1 つはスレッド専用領域です。共有領域にはメソッド領域とヒープ領域が含まれ、排他領域には Java 仮想マシン スタック、ローカル メソッド スタック、プログラム計算機が含まれます。そのため、ヒープ内のコンテンツは共有され、スタック内のコンテンツは排他的であることを誰もが覚えています。したがって、仮想マシン スタック上に定義したデータには気軽にアクセスできません。Final でデータを変更する場合は、スレッド A の仮想マシン スタックからメソッド領域にデータをコピーし、スレッド 2 がメソッド領域のデータにアクセスできるようにします。

スレッドのライフサイクル:

スレッド -> 実行可能 -> 実行中 -> 終了。

実行後は、wait() メソッドを使用して待機状態に入ることができます。待機状態にあるスレッドは、notify() または NoticeAll() メソッドによって起動され、Runnable() 状態に再度入ることができます。

 ロックがない場合、スレッドが作成されて start() が呼び出されると、実行可能な Runable() 状態になります。その後、スレッドがタイム スライスをプリエンプトすると、実行状態になります。つまり、すでに実行状態になっています。実行が完了すると、Termianted に到達し、終了となります。終了前に待ち状態が呼び出された場合は待ち状態になります。次に、他のスレッドがウェイクアップするまで待ちます (notify または NoticeAll を呼び出します)。その後、実行可能状態に入ります。このとき、タイム スライスがプリエンプトされた場合は、再び実行状態に入ります。

もちろんロックなしです。ロックが関係する場合、実際には非常に単純です。つまり、追加のブロック状態が存在します。ブロックされました。

ロックの状態に関係する場合、スレッドがロックを取得すると、他のスレッドはブロック状態になり、スレッドがロックを解放すると、ブロックされたスレッドはロックを取得して実行可能状態になります。

これはスレッドの状態図です。たとえば、Thread.sleep を呼び出すと、制限時間状態を待機する Timed_waiting 状態になることに注意してください。

新しいスレッドを作成することがよくありますが、このスレッドは実際には Linux システムの一番下にあるスレッドです。これを追跡して、その start() メソッドを確認できます。

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        // Android-changed: Replace unused threadStatus field with started field.
        // The threadStatus field is unused on Android.
        if (started)
            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);

        // Android-changed: Use field instead of local variable.
        // It is necessary to remember the state of this across calls to this method so that it
        // can throw an IllegalThreadStateException if this method is called on an already
        // started thread.
        started = false;
        try {
            // Android-changed: Use Android specific nativeCreate() method to create/start thread.
            // start0();
            nativeCreate(this, stackSize, daemon);
            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 */
            }
        }
    }

try コード ブロックに重要な行があります。

  // Android-changed: Use Android specific nativeCreate() method to create/start thread.
            // start0();
            nativeCreate(this, stackSize, daemon);

ここでわかるように、これは Android がこのメソッドを変更し、ローカルに作成したものです。

// Android-changed: Use Android specific nativeCreate() method to create/start thread.
    // The upstream native method start0() only takes a reference to this object and so must obtain
    // the stack size and daemon status directly from the field whereas Android supplies the values
    // explicitly on the method call.
    // private native void start0();
    private native static void nativeCreate(Thread t, long stackSize, boolean daemon);

前のメモに従って、次のように翻訳しましょう。

//Android は、Android 固有のネイティブCreate() メソッドを使用してスレッドを作成/開始するようにこのメソッドを変更しました。

//上流のネイティブ メソッド start0() はこのオブジェクトのみを参照するため、取得する必要があります

// スタック サイズとデーモンの状態は Android によって提供される値から取得されます。

// このメソッドの実行を表示します

//プライベートネイティブメソッドstart0()

android8.0以前はstart0メソッドが使用され、それ以降はnatvieCreateメソッドが使用されていました。

 ネイティブ メソッド start0() をトレースします。

static JNINativeMethod methods[] = {
    {"start0",           "(JZ)V",        (void *)&JVM_StartThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(Ljava/lang/Object;J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};

JVM_StartThread() メソッドにマップされている start0 メソッドが表示されます。このメソッドは、メソッド名を通じて JVM 内の関数として見ることができます。この方法を見つけました:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;

  // We cannot hold the Threads_lock when we throw an exception,
  // due to rank ordering issues. Example:  we might need to grab the
  // Heap_lock while we construct the exception.
  bool throw_illegal_thread_state = false;

  // We must release the Threads_lock before we can post a jvmti event
  // in Thread::start.
  {
    // Ensure that the C++ Thread and OSThread structures aren't freed before
    // we operate.
    MutexLocker mu(Threads_lock);

    // Since JDK 5 the java.lang.Thread threadStatus is used to prevent
    // re-starting an already started thread, so we should usually find
    // that the JavaThread is null. However for a JNI attached thread
    // there is a small window between the Thread object being created
    // (with its JavaThread set) and the update to its threadStatus, so we
    // have to check for this
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      // We could also check the stillborn flag to see if this thread was already stopped, but
      // for historical reasons we let the thread detect that itself when it starts running

      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
      // Allocate the C++ Thread structure and create the native thread.  The
      // stack size retrieved from java is signed, but the constructor takes
      // size_t (an unsigned type), so avoid passing negative values which would
      // result in really large stacks.
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz);

      // At this point it may be possible that no osthread was created for the
      // JavaThread due to lack of memory. Check for this situation and throw
      // an exception if necessary. Eventually we may want to change this so
      // that we only grab the lock if the thread was created successfully -
      // then we can also do this check and throw the exception in the
      // JavaThread constructor.
      if (native_thread->osthread() != NULL) {
        // Note: the current thread is not being used within "prepare".
        native_thread->prepare(jthread);
      }
    }
  }

ここに焦点を当てましょう:

 jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
      // Allocate the C++ Thread structure and create the native thread.  The
      // stack size retrieved from java is signed, but the constructor takes
      // size_t (an unsigned type), so avoid passing negative values which would
      // result in really large stacks.
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz);

上記のメモを翻訳すると、次のようになります。

// C++ スレッド構造を割り当て、ネイティブ スレッドを作成します。このスタックは Java から取得され、署名されています。ただし、コンストラクターには署名なしが必要です。したがって、スタック サイズが非常に大きくなる負の値を渡すことは避けてください。

(符号付きビットの場合、最初のビットは符号ビットです。符号ビットが符号なし表現に渡されると、最初の符号ビットも数値とみなされ、データが非常に大きくなります。)

ここで Java スレッドが作成され、仮想マシン スタックのサイズも渡されます。したがって、仮想マシン スタック (高速バッファ内) のサイズもスレッドのデフォルト サイズになります。この JavaThread() メソッドのトレースを続けてみましょう。

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
  Thread()
#if INCLUDE_ALL_GCS
  , _satb_mark_queue(&_satb_mark_queue_set),
  _dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{
  if (TraceThreadEvents) {
    tty->print_cr("creating thread %p", this);
  }
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
                                                     os::java_thread;
  os::create_thread(this, thr_type, stack_sz);
  _safepoint_visible = false;
  // The _osthread may be NULL here because we ran out of memory (too many threads active).
  // We need to throw and OutOfMemoryError - however we cannot do this here because the caller
  // may hold a lock and all locks must be unlocked before throwing the exception (throwing
  // the exception consists of creating the exception object & initializing it, initialization
  // will leave the VM via a JavaCall and then all locks must be unlocked).
  //
  // The thread is still suspended when we reach here. Thread must be explicit started
  // by creator! Furthermore, the thread must also explicitly be added to the Threads list
  // by calling Threads:add. The reason why this is not done here, is because the thread
  // object must be fully initialized (take a look at JVM_Start)
}

ここでスレッドを作成するメソッドは os::create_thread() であることがわかります。ここで渡されるスレッドのサイズはスタック サイズ staek_sz です。引き続きこのメソッドを見てください。

bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
  assert(thread->osthread() == NULL, "caller responsible");

  ...

  // stack size
  if (os::Linux::supports_variable_stack_size()) {
    // calculate stack size if it's not specified by caller
    if (stack_size == 0) {
      stack_size = os::Linux::default_stack_size(thr_type);

      switch (thr_type) {
      case os::java_thread:
        // Java threads use ThreadStackSize which default value can be
        // changed with the flag -Xss
        assert (JavaThread::stack_size_at_create() > 0, "this should be set");
        stack_size = JavaThread::stack_size_at_create();
        break;
      case os::compiler_thread:
        if (CompilerThreadStackSize > 0) {
          stack_size = (size_t)(CompilerThreadStackSize * K);
          break;
        } // else fall through:
          // use VMThreadStackSize if CompilerThreadStackSize is not defined
      case os::vm_thread:
      case os::pgc_thread:
      case os::cgc_thread:
      case os::watcher_thread:
        if (VMThreadStackSize > 0) stack_size = (size_t)(VMThreadStackSize * K);
        break;
      }
    }

    stack_size = MAX2(stack_size, os::Linux::min_stack_allowed);
    pthread_attr_setstacksize(&attr, stack_size);
  } else {
    // let pthread_create() pick the default value.
  }

  ...

    pthread_t tid;
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);

    pthread_attr_destroy(&attr);

    ...
}

このメソッドでは、Java のスレッド サイズがデフォルトで ThreadStatckSzie であることがコメントで示されていることがわかりますが、フラグ -Xs を設定することで変更できます。

このデフォルトのサイズは定数によって定義されます。

const int os::Linux::_vm_default_page_size = (8 * K);

つまり、デフォルトの 8K

したがって、仮想マシン スタックのデフォルト サイズが 8K であれば、高速バッファのデフォルト サイズも 8K にすることができます。

pthread_create メソッドはこのメソッドの後半で呼び出されることに注意してください。このメソッドは Linux とユニットがスレッドを作成するために使用するメソッドであるため、Android でのスレッド作成の究極の本質は依然として Linux システムに属するスレッドです。

次に、nativeCreate() メソッドを見て、追跡してみましょう。java_lang_Thread.cc ファイル内:

static JNINativeMethod gMethods[] = {
  FAST_NATIVE_METHOD(Thread, currentThread, "()Ljava/lang/Thread;"),
  FAST_NATIVE_METHOD(Thread, interrupted, "()Z"),
  FAST_NATIVE_METHOD(Thread, isInterrupted, "()Z"),
  NATIVE_METHOD(Thread, nativeCreate, "(Ljava/lang/Thread;JZ)V"),
  NATIVE_METHOD(Thread, nativeGetStatus, "(Z)I"),
  NATIVE_METHOD(Thread, nativeHoldsLock, "(Ljava/lang/Object;)Z"),
  FAST_NATIVE_METHOD(Thread, nativeInterrupt, "()V"),
  NATIVE_METHOD(Thread, nativeSetName, "(Ljava/lang/String;)V"),
  NATIVE_METHOD(Thread, nativeSetPriority, "(I)V"),
  FAST_NATIVE_METHOD(Thread, sleep, "(Ljava/lang/Object;JI)V"),
  NATIVE_METHOD(Thread, yield, "()V"),
};

次の行に注目してください。

  NATIVE_METHOD(Thread,nativeCreate, "(Ljava/lang/Thread;JZ)V")、navtiveCreate は、java_lang_thread.cc ファイルの Thread_nativeCreate 関数にマップされます。

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size,
                                jboolean daemon) {
  // There are sections in the zygote that forbid thread creation.
  Runtime* runtime = Runtime::Current();
  if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) {
    jclass internal_error = env->FindClass("java/lang/InternalError");
    CHECK(internal_error != nullptr);
    env->ThrowNew(internal_error, "Cannot create threads in zygote");
    return;
  }

  Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}

最後に、Thread.cc ファイルにある Thread::CreateNativeThread 関数が呼び出されます。

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
  CHECK(java_peer != nullptr);
  Thread* self = static_cast<JNIEnvExt*>(env)->self;

  ...

  Thread* child_thread = new Thread(is_daemon);
  // Use global JNI ref to hold peer live while child thread starts.
  child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
  stack_size = FixStackSize(stack_size);

  ...

  int pthread_create_result = 0;
  if (child_jni_env_ext.get() != nullptr) {
    pthread_t new_pthread;
    pthread_attr_t attr;
    child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
    CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread");
    CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED),
                       "PTHREAD_CREATE_DETACHED");
    CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size);
    pthread_create_result = pthread_create(&new_pthread,
                                           &attr,
                                           Thread::CreateCallback,
                                           child_thread);
    CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");

    if (pthread_create_result == 0) {
      // pthread_create started the new thread. The child is now responsible for managing the
      // JNIEnvExt we created.
      // Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization
      //       between the threads.
      child_jni_env_ext.release();
      return;
    }
  }

こんな一文がある。

stack_size = FixStackSize(stack_size);

これによりスタック サイズが取得されます。FixStackSize メソッドを見てみましょう。

static size_t FixStackSize(size_t stack_size) {
  // A stack size of zero means "use the default".
  if (stack_size == 0) {
    stack_size = Runtime::Current()->GetDefaultStackSize();
  }

  // Dalvik used the bionic pthread default stack size for native threads,
  // so include that here to support apps that expect large native stacks.
  stack_size += 1 * MB;

  ...

  return stack_size;
}

ここでわかるように、デフォルトのサイズは 1M で、以前よりも大幅に大きくなっています。

上記の Thread::CreateNativeThread メソッドを振り返ってみましょう。このコードは次のとおりです。

pthread_create_result = pthread_create(&new_pthread,
                                           &attr,
                                           Thread::CreateCallback,
                                           child_thread);

ここで、最後に pthread_create メソッドが再度呼び出されます。LINUX のスレッド作成メカニズムでもあります。

次に、同期の原理を見てみましょう (これは非常に重要です)。

2 つのスレッドが同時に a をインクリメントする状況をシミュレートしてみましょう。

package com.example.myapplication.Thread;

public class LockRunnable implements Runnable{
    private static int a = 0;
    @Override
    public void run() {
        for (int i= 0; i< 100000;i++){
            a ++;
        }
    }

    public static void main(String[] args) {
        LockRunnable lockRunnable = new LockRunnable();
        Thread thread1 = new Thread(lockRunnable);
        Thread thread2 = new Thread(lockRunnable);
        thread1.start();
        thread2.start();
        try{
            thread2.join();
            thread1.join();
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(a);
    }
}

結果がどうなるかを推測してみましょう:

 20000ではなく、118161です。

これはスレッドアンセーフの問題です。理由は何ですか?分析してみましょう:

実際、2 つのスレッドは同期されていません。つまり、スレッド 2 はスレッド 1 が run メソッドの実行を完了するのを待たず、途中で実行するように追加しました。スレッド 1 が a を自動インクリメントした後、増加した値をメソッド領域に書き込んでいない場合、スレッド 2 がメソッド領域から取得した値はまだ自動インクリメント前の値です。たとえば、前の値が 1 だったスレッド 1 は、自己インクリメント後は 2 になります。ただし、メソッド領域には更新されません。スレッド 2 が取得するのは 1 のままです。スレッド 2 はそれに 1 を加えて 2 になります。その後、メソッド領域に書き込みます。a の値は 2 になります。次に、スレッド 1 は新しくインクリメントされた値 2 をメソッド領域に書き込みますが、a の値は 2 のままです。aは2倍になっていますが、1から2になっただけです。

この時点で、ロックしてみましょう。

  @Override
    public void run() {
        for (int i= 0; i< 100000;i++){
            add();
        }
    }

    private synchronized static void add(){
        a ++;
    }

結果を見てみましょう:

 ここでロックされているのは、このメソッドを呼び出すオブジェクトであることに注意してください。同じオブジェクトのロックのみが機能します。

synchronized キーワードを分析します。メソッドをロックすると、最終的にコンパイルされたバイトコード ファイルにいくつかの命令が表示されます。

モニター開始 v1 とモニター終了 v1。

仮想マシン内のこのモニターに関する機能を見てみましょう。

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

これは、UseBiasedLocking が偏ったロックであるかどうかを意味し、偏ったロックである場合には、ロックを迅速に取得するロジックを実行することがわかります。

synchronized キーワードを使用してメソッドを変更すると、スレッドがメソッドを呼び出すと、ロック オブジェクトが取得されて実行されることがわかっています。他のスレッドは、ロックを競合する前に、スレッドの実行が完了するまで待つ必要があります。物体。このロジックに非常に時間がかかると、実行効率が非常に低くなります。したがって、この状況を最適化するために、Object オブジェクトが定義され、同期する必要があるコードがロックされます。

  private Object lockObject = new Object();
    @Override
    public void run() {
        for (int i= 0; i< 100000;i++){
            add();
        }
    }

    private  void add(){
        //。。。。其他耗时代码 开始
        //。。。。其他耗时代码 结束
        synchronized (lockObject){
            //需要同步的代码
            a ++;
        }
    }

この場合、効率はより高くなります。オブジェクトをロックできるため、任意のオブジェクトをロックできます。オブジェクトのロック状態情報がどのように記録されるかを見てみましょう。

オブジェクトがヒープメモリ領域にあるためです。したがって、オブジェクトのメモリ構造を知ることができます。

 オブジェクトのロック状態情報はオブジェクトヘッダーに記録されます。

オブジェクトのオブジェクト ヘッダーには、ロックに加えて、ガベージ コレクション メカニズムの情報も記録されます。たとえば、古い世代、若い世代、ハッシュコードです。レコード ロック ステータスは、32 ビットを使用するか 64 ビットを使用するかを決定するためにシステムによって決定されます。

 一般に、ここにはロック フラグがあります。2 つまたは 3 つのスレッドが競合する場合、それは軽量ロックです。偏ったロックにはロック ID が記録されます。EPOCH は、タイムアウトしたかどうかを判断するためのタイムスタンプです。タイムアウトするとアンロック状態となります。タイムアウトがない場合は、プリエンプションがスローされることを意味します。多すぎると重くなります。

おすすめ

転載: blog.csdn.net/howlaa/article/details/128443647