豁然开朗篇:安卓开发中关于线程那些事(上篇)

彻底搞懂线程这一块,看这一篇就够了


前言

在我的豁然开朗篇:安卓开发中关于内存那些事曾经简单提到过一个CPU,它里面每个核处理一个线程,而每个核又有自己私有的高速缓冲区,这些概念跟线程有什么关系,也将会一一讲解,希望大家读完本文之后对线程有一个更深的理解,彻底能把线程、锁以及线程池等相关知识彻底搞懂。


提示:以下是本篇文章正文内容

一、并发

并发其实就是同时做几件事情,后台文件下载,网络获取,图片加载,数据库读写,一些耗时的业务逻辑。

所以并发能提高资源利用率,开5个线程来执行该任务,更快地完成;程序上也更清晰精简,每个线程的分工明确;主线程能更好地处理其他事情,避免ANR。

不过并发也有不好的地方,那就是需要占用更多资源,线程安全问题(线程同步,执行结果不符合预期,异常等)

1.安卓中的并发应用场景

1)加载bitmap,BitmapFactory.decodeFile()
2)获取大量数据库数据
3)读取IO,比如shareprence使用子线程来apply()
4)App启动加载一些配置,比如xml解析

以上这些场景都需要用子线程来处理。

2.Thread

说到并发,就不能不提Thread和synchronized,就是线程和锁。下面就来讲解一下Thread。

进程与线程的关系

抛开那些官方抽象的概念,其实进程就是你手机里运行的每个应用,它们都是一个个的进程,而线程则是进程中对应的一条任务的执行控制流,也就是说一个进程里有多个线程,如果将一个生产车间比喻成一个进程的话,那么一个生产车间里的多条生产线便是多个线程。

高速缓冲区

在最早的时候,计算机只有进程,同一时间内,是一个进程对内存进行操作,但这样太低效了,然后并发这一概念就出来了,可以多个进程进行轮换地切换,以达到“同时”对内存进行操作:
在这里插入图片描述
但这样还是有个问题,便是进程与进程间的切换需要耗费很多资源,因此为了更加高效,便设计出线程,将内存中的数据缓存在高速缓冲区里,每个线程都有其对应的高速缓冲区,线程往高速缓冲区里存取数据,这样原先的进程间切换就变成了线程间的切换,更加轻量化了,从而减少资源耗损:
在这里插入图片描述
每个线程里有独立的高速缓冲区,也就是寄存器,可以理解为CPU里的内存,一核代表一个线程,所以假设一个4核的8G手机,如图所示:
在这里插入图片描述
可能会有人产生疑问,既然是一个核对应一个线程,为什么平时我们看到的好像有很多个线程,因为CPU的计算速度太快,能迅速地进行线程间的创建与切换,把时间拉宽来看,就好像有多个线程在同时执行,实际上,如果把时间拉短来看,它还是执行完该线程后才去执行另一个线程。

现在再来结合豁然开朗篇:安卓开发中关于内存那些事这张内存结构图:
在这里插入图片描述
现在可以知道,一个进程内,方法区和堆区就是在主内存里,而虚拟机栈则在高速缓存区里,所以只要是定义在虚拟机栈的数据,彼此间线程都不能随便访问的,用final修饰数据,将数据从线程A的虚拟机栈中复制到方法区里,这样线程2就能从方法区里访问该数据,也就是这样的原理。

线程的生命周期

下面来看张图:
在这里插入图片描述
在没有锁的情况下,当一个Thread创建后,start()调用后就会变成Runnable状态,然后当该线程抢到时间片后,就会Running状态(即开始运行),当执行完毕后便会结束Terminated,当然在运行中如果调用wait()后便会转为等待状态Waiting,然后一直到其他线程调用notify()或者notifyAll()才会被唤醒会,进入Runnable状态,然后抢时间片,重新Running。

如果涉及到锁的时候,当该线程抢到锁后,其他线程便会处于Blocked状态,等到该线程释放锁之后,那些阻塞的线程拿到锁后进入Runnable状态,然后Running:
在这里插入图片描述

线程的本质

经常new的Thread其实底层是Linux系统的线程,我们可以一步一步来跟踪,首先看Thread.java里看的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 */
            }
        }
    }

其中可以看到关键的一行:nativeCreate(this, stackSize, daemon),很明显跟创建线程有点关系,继续跟踪它:

// 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用来创建线程的方法,它之前是使用start0()方法,现在使用了nativeCreate()方法,都是属于native层,所以它们的JNI具体实现都要去安卓源码去找。先来说说start0()方法,它的JNI是在安卓源码(以8.0.0版本来讲)的android-8.0.0 _r1/libcore/ojluni/src/main/native目录的Thread.c可以找到:

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_StartThread函数看字样应该就知道它是JVM调用的函数,所以去jvm.cpp去找它,如果安卓源码里找不到jvm.cpp可以直接去jdk源码找:hotspot/src/share/vm/prims:

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);
      }
    }
  }

可以看到这里构造了JavaThread对象,并且传了栈大小进去,所以我们的虚拟机栈(在高速缓冲区)的大小也就是线程的默认大小,那么继续跟踪new JavaThread(&thread_entry, sz)方法,具体实现是在thread.cpp文件里,它的目录是hotspot\src\share\vm\runtime里:

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(this, thr_type, stack_sz),系统内核调用函数创建线程,传的线程大小就是栈大小stack_sz,而os::create_thread函数是在目录hotspot\src\os\linux\vm下的os_linux.cpp文件里:

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

    ...
}

可以看到,首先是要根据不同情况来定义这个线程大小是多少,默认大小为8k:

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

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

接下来继续看,调用了pthread_create()方法,这个方法就是Linux/Unix用来创建线程的方法,所以其实安卓的线程本质还是属于Linux系统的线程那一套东西。

这是start0()方法的讲解,而现在安卓不用它,而改用nativeCreate(),所以我们来追踪它,它也是native方法,它的JNI实现是在android-8.0.0_r1\art\runtime\native目录里的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”),其实nativeCreate被映射成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()函数,这个函数是在android-8.0.0_r1\art\runtime目录下的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()函数后赋值的,因此来看看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,也就是1024k,跟之前start0()版本时候的8k要大得多,这也是它们的区别之一,我们再往回看Thread::CreateNativeThread()函数,可以看到接下来就是调用了

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

没错,还是调用了pthread_create()函数,证明了还是使用Linux系统创建启动线程的那一套机制。

3.synchronize原理

下面来模拟一下两个线程来同时对a进行自增操作的场景:

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 lockRunnable1 = new LockRunnable();
        //LockRunnable lockRunnable2 = new LockRunnable();
        Thread thread1 = new Thread(lockRunnable1);
        Thread thread2 = new Thread(lockRunnable1 );
        thread1.start();
        thread2.start();
        try {
            thread2.join();
            thread1.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(a);
    }
}

按照正常思维最后输出的结果应该是200000,但实际上运行结果变成166924:
在这里插入图片描述
这就是我们平常所说的线程不安全问题,因为两个线程没有同步(就是线程2没有等线程1执行完run方法就中途加入进去执行),这样线程1在执行run方法时,对a进行自增操作,因为我们调用了thread2.join(),让线程1处于等待状态,然后线程2开始执行run方法,如果线程1等待的时机是在a已经加1之后那刻,那么也就是说线程1还没有将自增后的结果更新到方法区中的a里去,而此时线程2还是根据原来方法区里a的值1来进行自增的,所以对它进行完自增后为2,然后更新到方法区里的a值为2,而此时线程1恢复,继续刚刚的行为,把它刚刚自增的结果1更新到方法区里的a,那此时a的值又从2变为1,这样循环下去,最后结果肯定跟我们预想的200000不一样的,这就是我们平常所说的并发引起的问题,线程没有同步,所以就需要锁来对它们进行同步:

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

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

这样结果就如我们所期望是200000了:
在这里插入图片描述
这样添加了synchronized后,锁的就是调用该方法的当前对象,也就是LockManiu对象,因此线程1和线程2也就同步了。当然如果此时该两个线程绑定的是不同的对象,那么此时synchronized也就没用,我们需要锁的是同一个对象才行。

分析synchronized

怎么去分析synchronized,它是一个关键字,追踪不到它的源码,可以换种思路,分析它的字节码指令,看看它最后被编译成字节码文件后,synchronized在里面是怎样的呈现,所以我们先定义一个Person类:

public class Person {

    public Person() {

    }

    public synchronized void run() {
        int a = 1;
    }
}

接下来要用到dex工具:
在这里插入图片描述
然后把Person类的.class文件用dex工具将它编译成dex文件,也就是安卓的字节码文件,使用以下这个命令:
在这里插入图片描述
使用的是一个命令:dx --dex --verbose --dump-to=Person.dex.txt --dump-method=Person.run --verbose-dump Person.class,把Person类文件里的方法(也就是run方法)编译成dex指令后输出到一个txt文件里,然后我们去看看这个加入了synchronized修饰的方法是怎样的:
在这里插入图片描述
没有汇编基础的同学也没关系,这里已经为大家删减了一些不需关心的指令了,然后再来编译一个没有用synchronized修饰的run方法的dex指令集文件:
在这里插入图片描述

通过对比没有加synchronized修饰编译后的同样一个方法,可以看出,修饰了synchronized的方法的指令是有区别的,仔细一看就能留意到这两行:monitor-enter v1和monitor-exit v1,所以可以认为,当加入了synchronized后,指令集里就会多出这两行指令,表示这个方法使用了锁机制的。既然是指令,那它背后肯定是C/C++函数,所以去虚拟机源码中的interpreterRuntime.cpp文件里寻找关于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如果成立,则执行fast_enter函数,其实通过字面翻译就可以知道,这个UseBiasedLocking代表是否是偏向锁,是的话,则执行快速得到锁对象的逻辑。

说到偏向锁,那就不得不先说一下锁机制,我们都知道,用synchronized去修饰一个方法时,线程去调用该方法时,就会获取锁对象,然后执行,其他线程则要等该线程执行完才能去竞争这个锁对象,但如果这个方法里的逻辑很多,非常多代码,那么这样就会使得执行效率变慢,所以为了减轻锁带来的压力,会定义一个Object对象,然后锁上需要同步的代码块:

public class LockRunnable implements Runnable{

    private static int a = 0;

    private Object lockObject = new Object();

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

    private void add() {
        a++;
        synchronized (lockObject) {
            //需要同步的代码
        }
    }
    ...
}

这样执行效率就会高很多,既然如此,就可以知道锁是可以锁任意对象的,那么这样就可以通过所有对象都有一个共同的父类Object来记录一个对象的锁状态信息,然后通过这些记录信息就可以知道对象是否是有锁。下面是Object的源码:

public class Object {

    private static native void registerNatives();
    static {
        registerNatives();
    }

	public final native Class<?> getClass();

	public native int hashCode();
	...
	public final native void notify();
	...
}

它里面的方法就不一一展示,但可以看到大多数方法都是native,因此我们就要从内存的角度去分析Object,看看它里面是怎样记录锁状态的。

锁的对象的内存结构

因为对象是在堆区里,如果之前看过我的文章豁然开朗篇:安卓开发中关于内存那些事就会知道内存里的对象结构是怎样的,它是由对象头、实例数据和对其填充数据组成的,而一个对象的锁状态信息就是记录在对象头里的。
在这里插入图片描述

下面来创建一个对象,然后对这个对象打个断点:
在这里插入图片描述

通过断点信息可以看到person对象这两个信息klass_和monitor_:
在这里插入图片描述

klass其实是person对象在内存里的映射,它里面包含着一个对象里各种信息的集合:
在这里插入图片描述

其中可以看到objectSize也就是对象大小为8字节,我们定义的person是没有定义属性,所以实例对象是空的,但即使这样,它本身还是占8字节大小,因为对象头里本身存储着一些数据,那么对象头里存储的数据其中就有记录锁的相关信息,还有垃圾回收机制的相关信息,比如对象是老年代还是年轻代以及hashcode等这些信息,其中记录锁状态是用了32位/64位(由系统位数决定)来记录这些信息的:
在这里插入图片描述

我们以32位系统来讲,当一个对象是无锁状态时,那么此时前25位存的是hashcode,它是一个对象的内存地址,通过hashCode()方法获取:

// public native int hashCode();
    public int hashCode() {
        return identityHashCode(this);
    }

继续跟踪identityHashCode()方法:

static int identityHashCode(Object obj) {
        int lockWord = obj.shadow$_monitor_;
        final int lockWordStateMask = 0xC0000000;  // Top 2 bits.
        final int lockWordStateHash = 0x80000000;  // Top 2 bits are value 2 (kStateHash).
        final int lockWordHashMask = 0x0FFFFFFF;  // Low 28 bits.
        if ((lockWord & lockWordStateMask) == lockWordStateHash) {
            return lockWord & lockWordHashMask;
        }
        return identityHashCodeNative(obj);
}

首先有个lockWord变量是通过对象的shadow_monitor_赋值而来,这个shadow_monitor_是不是很熟悉,我们刚刚通过断点也看到了它,而且monitor这个单词好像又与我们查看synchronized字节指令很像,接着往下看可以发现通过判断它来返回lcokWord或者identityHashCodeNative()方法,所以可以知道其实整段逻辑就是判断是否有锁,如果无锁,则返回identityHashCodeNative()方法来获得hashCode,这其中肯定就是从无锁时的对象的对象头里的前25位这里获取。
在这里插入图片描述
25位后面接着就是4位,存储的是垃圾回收机制的年龄代信息,再接着的1位记录的是是否偏向锁(0否,1是),所以上面刚刚synchronized字节指令源码里的那个UseBisedLocking就是从这1位里获取的,最后2位记录的锁标志位,记录的是对象的锁程度类型。

有锁的状态下,这时对象头里的存储结构就会发生变化,根据identityHashCode()方法来看,可以知道当有锁的时候,去获取hashCode就不走identityHashCodeNative()方法了,而是走上面的返回lockWord&lockWordHashMask,其实这里返回的是缓存数据,通过shadow$_monitor_已经缓存好了,所以直接复用就可以,这就让有锁的状态下不用再次重新调用一次identityHashCodeNative()方法,复用缓存,非常巧妙,又节省内存,不用那么多位来存储hashcode。

在这里插入图片描述

如果是有锁的情况下首先是偏向锁,这样前23位记录的是线程id,这样下次轮到这个线程又来竞争该锁对象时,就会通过这个记录来对比,从而判断是否对该线程偏向给它,然后后面2位记录的Epoch,可以理解为是一个时间戳。后面1位的值是1,表示是偏向锁,然后最后2位状态标志仍然是01。Epoch表示该线程抢占锁对象的时间是否超时,超时的话就会变回无锁状态,没超时表示仍在抢占,那么此时就进入下一个锁升级,变成轻量级锁(通常2-3个线程间在竞争锁对象):

在这里插入图片描述

前30位记录的是指向线程的地址,看过我的豁然开朗篇:安卓开发中关于内存那些事就应该知道,线程其实就是虚拟机栈,那么这个线程的地址可以看成是栈的地址,因为是线程间在竞争锁对象,那么肯定是要知道栈的地址,才能将锁对象给哪个线程。最后两位就是轻量级锁的标志位,值是00。

如果是多个线程来竞争同一个锁对象,那么此时轻量级锁就会升级为重量级锁了,重量级锁的申请开销最大,因为它是由原来线程主动去抢锁对象变成由系统内核来决定最后锁对象分配给谁,这是一个用户空间变成内核空间的过程,而一旦涉及到内核区,性能开销就会很大。所以开始时候这些线程还是在这个轻量级锁的时候,每个线程会自旋,可以理解为线程会等待一段时间,然后来抢占这个锁对象,而当每个线程在自旋限定的次数之后仍然抢不到这个锁对象时,锁对象就升级为重量级锁,改由系统内核来决定分配给哪个线程来执行。
在这里插入图片描述

那么它的对象头里是这样分布的,前30位存储的是指向monitor的指针,它是一个指向内核的地址,这个内核区里的monitor就是ObjectMonitor,它来管理锁然后分配锁给哪些线程。最后2位的标志位值是10。

ObjectMonitor

要找ObjectMonitor,就要找虚拟机的源码,它在hotspot\src\share\vm\runtime目录下的objectMonitor.cpp文件里:
在这里插入图片描述
代码很长,就不仔细分析它,我们来看一下它的h文件objectMonitor.hpp:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

通过它的初始化函数可以看到很多属性,其中_EntryList很明显就是竞争锁对象的线程的集合,_WaitSet就是需要等待的线程集合,而_count则是线程获得该锁对象的次数等等这些数据都证明了ObjectMonitor就是用来实现synchronized机制,对锁对象的管理作用,由它来决定把锁对象分配给哪些线程。
在这里插入图片描述

二、总结

本篇算是抛砖引玉,给大家全方位讲解并发的知识,其实就是线程与锁的知识,都是一些必须要掌握的基础与原理,虽然是上篇,但可以当做总纲来看,而有些知识点的扩展则会在下篇继续给大家详细讲解,因为实现并发可不止只有synchronized关键字这一种方法,还有Atomic、Locks等方式,还有一些涉及到锁原理的扩展知识对于以后要自定义锁会有很大帮助,以及线程池、上文提到的klass等这些知识也是一个优秀的开发者必须要掌握的知识点,这些都会在下篇为大家详细讲解。

更多有趣的文章可以关注本人公众号:Pingred
继续学习与讨论

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值