Android并发编程里的线程原理

Posted 呼啸

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android并发编程里的线程原理相关的知识,希望对你有一定的参考价值。

1.进程和线程的概念

抛开那些官方的概念,我们可以大致理解为:进程就是手机里运行的一个个应用,他们都是一个个的进程(当然,有些App是多进程的,这个先不谈)。线程则是进程中对应的一个任务的执行控制流。如果将一个进程比喻成一个车间的话,那么这个车间里的每个生产线就可以看作是一个线程。

高速缓冲区的概念:

在最早的时候,计算机只有进程。同一个时间内,是一个进程对内存进行操作。但这样效率比较低。比如我们要进程一个文件的读写操作,需要等这个操作完成,才能干别的事。并发概念出来后,可以多个进程进行轮换切换,以达到相对意义上的“同时”对内存进行操作。

但是这样有一个问题,就是进程与进程之间的切换需要耗费很多资源。因此为了更加的高效,便设计出了线程,将内存中的数据存在高速缓冲区里,每个线程都有其对应的高速缓冲区,线程往高速缓冲区里存取数据,这样原先的进程间切换就变成了线程间的切换,更加轻量化了,从而减少资源耗损。

每个线程都有独立的高速缓冲区,就是寄存器。可以理解为CPU里的内存。一核代表了一个线程,所以假设一个4核8G的手机,一个2核就有2G。

我们知道,平时我们写的java文件,通过javac把.java文件变成.class文件,然后再通过类加载器,classLoader把.class文件加载到内存中,而jvm在运行这个.class字节码文件的时候,会进行一些内存划分。

这里面有主要两块的划分,一个是线程共享区,一个是线程独占区。 共享区就包括了方法区和堆区,而独占 区就包括了,java虚拟机栈,本地方法栈,程序计算器。所以大家记住了,堆里面的内容是共享的,而栈里的内容是独占的。所以我们定义在虚拟机栈的数据,彼此之间是不能随便访问的。如果我们用final修饰数据,就将数据从线程A的虚拟机栈中复制到方法区里,这样线程2就能从方法区里访问这个数据。

线程的生命周期:

Thread -> Runnable ->Runing ->Terminated.

在Running之后,也可以经过wait()方法进入到waiting状态。在waiting状态的线程,可以通过notify()或者notifyAll()方法唤醒,使其重新进入Runnable()状态。

 在没有锁的情况下,当一个Thread创建之后,start()调用后,就变成了Runable()状态,也就是可执行情况。然后当该线程抢占到时间片后,就变成了Running状态。也就是已经在执行状态了。当执行完成后就到了Termianted,也就是结束了。如果在还没结束的时候,调用了wait状态,就会进入等待waiting状态。然后一直等到其他线程唤醒他,也就是调用notify或者notifyAll.然后进入runnable状态,这个时候,然后如果抢占到时间片,就会再进入running状态。

当然,这是没有锁的情况下。如果涉及到锁,其实也很简单,就是 多了一个阻塞状态。blocked.

如果涉及到锁的状态,当该线程抢到锁之后,其他的线程就会进入Blocked状态,等到该线程释放锁之后,那些阻塞的线程在拿到锁喉进入Runnable状态。

这是一个线程的状态图。需要注意的是, 比如我们调用了Thread.sleep,他就会进入Timed_waiting状态,也就是等待有限时间状态。

我们经常new一个线程,这个线程其实底层是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特定的nativeCreate()方法创建/启动线程。

//上游的native方法start0()仅仅引用了这个对象,因此必须获得

//堆栈大小和守护进程状态来自android提供的值。

//在此方法上显示执行

//私有native方法start0()

在android8.0之前是使用start0方法,之后使用natvieCreate方法。

 我们跟踪start0()这个native方法:

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,
;

可以看到start0方法,映射为JVM_StartThread()方法,这个方法通过方法名可以看到属于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.可以通过 flag -Xs来设置更改。

这个默认的大小,是有一个常量来定义的:

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

也就是默认8K

所以我们可以默认为如果虚拟机栈的默认大小是8K,所以高速缓冲区的默认大小也是8K。

注意看,这个方法里后面调用了pthread_create方法。这个方法就是linux和unit用来创建线程的方法,所以安卓里创建线程,最终的本质还是属于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::CreateNativeThread函数,这个函数在Thread.cc文件里:

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的线程创建机制。

接下来我们看看synchrozie的原理(这点很重要):

我们来模拟下两个线程同时对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没有等待线程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关键字,如果大家对一个方法加锁的话,最终编译的字节码文件,可以看出了一些指令:

monitor-enter v1 和 monitor -exit v1。

再看下关于虚拟机里关于这个monitor的函数:

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是否是偏向锁,如果是的话,则执行快速得到锁的逻辑。

我们知道,如果我们用sychronized关键字修饰一个方法,线程去调用该方法的时候,就会获取锁对象,然后执行,其他线程则要等该线程执行完才能去竞争这个锁对象。如果这个逻辑非常耗时,那么执行效率就会变得很低。所以为了优化这个情况,会定义个Object对象,然后锁上需要同步的代码:

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

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

这样的话,效率高一些。既然可以锁住Object,那么就可以锁住任何对象。我们来弄清楚,一个对象的锁状态信息是如何记录的:

因为对象是在堆内存区的。所以我们可以知道对象的内存结构:

 对象的锁状态信息就是记录在对象头里的。

对象的对象头记录的信息比较多,除了锁,还记录了垃圾回收机制的信息。比如是老年代,还是年轻代,还有hashcode等。记录锁状态是根据系统来决定用32位还是64位决定的:

 总的来说,就是这里有锁标志位,当两三个线程竞争的时候,是轻量级锁,偏向锁那里记录了锁的ID,EPOCH是一个时间戳,判断是否超时。超时的话就变成无锁状态。没超时表示扔在抢占。多的话就变成重量级。

以上是关于Android并发编程里的线程原理的主要内容,如果未能解决你的问题,请参考以下文章

Android并发编程

并发01--并发存在的问题及底层实现原理

Java并发编程系列- JMM和底层实现原理

Day829.Java线程的生命周期 -Java 并发编程实战

Day829.Java线程的生命周期 -Java 并发编程实战

Java并发编程实战04对象的组合