Java 多线程 : 细说线程状态

1,548 阅读9分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…

前言

这一篇说一说线程状态

一. 线程等待

// 等待具体时间
> sleep(time) 
    // 该方式不释放锁 ,低优先级有机会执行
    // sleep 后转入 阻塞(blocked)
> wait(time)
> join(time)
> LockSupport.parkNanos()
> LockSupport.parkUnit()
> yield
	// 该方式同样不会释放锁 ,同优先级及高优先级执行
	// 执行后转入ready
	
// 仅 进入等待
> wait()
> join()
> LockSuppot.park()    

二. 线程通知

// 对于设定具体等待时间的 timeout 后自动转入就绪

// 其他等待
> notify()
> notifyAll()

> 不同线程之间采用字符串作为监视器锁,会唤醒别的线程
> 不同线程之间的信号没有共享,等待线程被唤醒后继续进入wait状态:
    
    
    
> 下图为不同线程的等待与唤醒    

notify.jpg

> 执行wait () 时释放锁 , 否则等待的线程如果继续持有锁 , 其他线程就没办法获取锁 , 会陷入死锁

// Wait - Notify 深入知识点
// 一 : Wait 等待知识点
- 当前线程必须拥有这个对象的监视器
    
    
// 二 : Wait 等待后    
- 执行等待后 , 当前线程将自己放在该对象的等待集中,然后放弃该对象上的所有同步声明 
- 如果当前线程在等待之前或等待期间被任何线程中断 , 会抛出 InterruptedException 异常    
    
    
// 三 : 唤醒时注意点
- Notify 唤醒一个正在等待这个对象的线程监控 (monitor)
- 执行 wait 时会释放锁 , 同时执行 notify 后也会释放锁 (如下图)
- notify 会任意选择一个等待对象来提醒

// 四 : 唤醒后知识点
- 线程唤醒后 , 仍然要等待该对象的锁被释放
- 线程唤醒后 , 将会与任何竞争该锁的对象公平竞争
    
    
// 假醒 : 
线程也可以在不被通知、中断或超时的情况下被唤醒,这就是所谓的伪唤醒。    
    
    
    
    

notify2.jpg

三. 线程中断

> interrupt() 
// 方法,用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。

> interrupted ()
// 查询当前线程的中断状态,并且清除原状态。
	
> isInterrupted ()
// 查询指定线程的中断状态,不会清除原状态+
    
// interrupt() 方法干了什么 ? 
public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
 }    

// 1 checkAccess() : 其中涉及到 SecurityManager , 所以我们先看看这个类干什么的
	- SecurityManager security = System.getSecurityManager();
	- security.checkAccess(this);
	
C- SecurityManager : 
	?- 这是 Java.lang 底下的一个类


    
    

四. 线程死锁

死锁简介 : 当多个进程竞争资源时互相等待对方的资源
死锁的条件 :

  • 互斥条件 : 一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  • 请求与保持条件 :进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  • 不可剥夺条件 : 进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
  • 循环等待条件 : 若干进程间形成首尾相接循环等待资源的关系
// 资源的分类
- 可抢占资源 : 可抢占资源指某进程在获得这类资源后,该资源可以再被其他进程或系统抢占 , 例如 CPU 资源
- 不可抢占资源 

// 死锁的常见原因 : 
- 竞争不可抢占资源引起死锁 (共享文件)
- 竞争可消耗资源引起死 (程通信时)
- 进程推进顺序不当引起死锁
    
// 死锁的预防
- 通过系统中尽量破坏死锁的条件 , 当四大条件有一个不符合时 , 死锁就不会发生
- 通过加锁顺序处理(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
- 死锁检测
    
// 死锁的解除
- 资源剥离 , 挂起死锁进程且强制对应资源 , 分配进程
- 撤销进程
- 回退进程    

五. 线程热锁

> 热锁不算一个物理概念 , 它表示线程在频繁的竞争资源并且资源在频繁的切换\
	- 循环等待 :  

六. 线程的状态及转换

> 线程有以下状态
    - NEW : 尚未启动的线程的hread状态
    - RUNNABLE : 可运行 , 从虚拟机的角度 , 已经执行 ,但是可能正在等待资源
    - BLOCKED : 阻塞 , 此时等待 monitor锁 , 以读取 synchronized 代码 
    - WAITING : 等待状态 , 处于等待状态的线程正在等待另一个线程执行特定操作
        - wait()
        - join()
        - LockSupport#park()
    - TIMED_WAITING : 指定等待时间的等待
        - Thread.sleep
        - wait(long)
        - join(long)
        - LockSupport#parkNanos
        - LockSupport#parkUntil
    - TERMINATED : 终止线程   
	
// 线程间状态改变的方式	
• 还没起床:sleeping 。
• 起床收拾好了,随时可以坐地铁出发:Runnable 。
• 等地铁来:Waiting 。
• 地铁来了,但要排队上地铁:I/O 阻塞 。
• 上了地铁,发现暂时没座位:synchronized 阻塞。
• 地铁上找到座位:Running 。
• 到达目的地:Dead 。		

状态的转换.jpg.png

xianc.png

七. 状态转换的原理

7.1 wait 与 notify 原理

// 节点一 : 你是否发现 , wait 和 notify 是 object 的方法
点开 wait 和 notify 方法就能发现 , 这两个方法是基于 Object 对象的 , 所以我们要理解 ,通知不是通知的线程 ,而是通知的对象
这也就是为什么 , 不要用常量作为通知对象    


// 节点二 : java.lang.IllegalMonitorStateException
当我们 wait/notify 时 , 如果未获取到对应对象的 Monitor , 实际上我们会抛出 IllegalMonitorStateException
所以你先要获得监视器 , 有三种方式 : 
- 通过执行该对象的同步实例方法。
- 通过执行在对象上同步语句体。
- 对于类型为Class的对象,可以执行该类的同步静态方法。    


// 节点三 : 如何进行转换 ? 
Step 1 : 首先看 Object 对象 Native 方法 , bative src 中搜索名字即可

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"wait",        "(J)V",                   (void *)&JVM_MonitorWait},
    {"notify",      "()V",                    (void *)&JVM_MonitorNotify},
    {"notifyAll",   "()V",                    (void *)&JVM_MonitorNotifyAll},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};

//可以看到这里分别调用了 JVM_MonitorWait , JVM_MonitorNotify , JVM_MonitorNotifyAll , 
//从名字就能看到 , 这里是和Monitor 有关的
    
Step 2 : 进入全路径了解 : \openjdk\hotspot\src\share\vm\prims
JVM_ENTRY(void, JVM_MonitorWait(JNIEnv* env, jobject handle, jlong ms))
  JVMWrapper("JVM_MonitorWait");
  Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
  JavaThreadInObjectWaitState jtiows(thread, ms != 0);
  if (JvmtiExport::should_post_monitor_wait()) {
    JvmtiExport::post_monitor_wait((JavaThread *)THREAD, (oop)obj(), ms);
	// 当前线程已经拥有监视器,并且还没有添加到等待队列中,因此当前线程不能成为后续线程
  }
  ObjectSynchronizer::wait(obj, ms, CHECK);
JVM_END
    
Step 3 :  ObjectSynchronizer::wait(obj, ms, CHECK);
// TODO : 看不懂了呀....先留个坑 
// 总得来说就是 ObjectMonitor通过一个双向链表来保存等待该锁的线程

Step End :  link by @ https://www.jianshu.com/p/a604f1a9f875 
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) {
  ...................
   // 创建ObjectWaiter,添加到_WaitSet队列中
   ObjectWaiter node(Self);
   node.TState = ObjectWaiter::TS_WAIT ;
   Self->_ParkEvent->reset() ;
   OrderAccess::fence();          // ST into Event; membar ; LD interrupted-flag

 //WaitSetLock保护等待队列。通常只锁的拥有着才能访问等待队列
   Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ;
 //加入等待队列,等待队列是循环双链表
   AddWaiter (&node) ;
   //使用的是一个简单的自旋锁
   Thread::SpinRelease (&_WaitSetLock) ;
   .....................
}

    

7.2 Thread run

// 节点一 : 区别 run 和 start 
run 是通过方法栈直接调用对象的方法 , 而 Start 才是开启线程 , 这一点我们可以从源码中发现 : 
- start 方法是同步的
- start0 是一个 native 方法
- group 是线程组 (ThreadGroup) , 线程可以访问关于它自己线程组的信息
    ?- 线程组主要是为了管理线程 , 将一个大线程分成多个小线程 (盲猜 fork 用到了 , 后面验证一下)
    ?- 线程组也可以通过关闭组来关闭所有的线程
    
public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
}

// Step 1 : Thread.c 结构 -> openjdk\src\native\java\lang
static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"stop0",            "(" OBJ ")V", (void *)&JVM_StopThread},
    {"isAlive",          "()Z",        (void *)&JVM_IsThreadAlive},
    {"suspend0",         "()V",        (void *)&JVM_SuspendThread},
    {"resume0",          "()V",        (void *)&JVM_ResumeThread},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield",            "()V",        (void *)&JVM_Yield},
    {"sleep",            "(J)V",       (void *)&JVM_Sleep},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"countStackFrames", "()I",        (void *)&JVM_CountStackFrames},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"isInterrupted",    "(Z)Z",       (void *)&JVM_IsInterrupted},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",        "()[" THD,   (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};

// \openjdk\hotspot\src\share\vm\prims\jvm.cpp
// Step 2 : JVM_StartThread , 翻译了一下 , 大概可以看到那一句 native_thread = new JavaThread(&thread_entry, sz);
// 以及最后的 Thread::start(native_thread);
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;

  //由于排序问题,在抛出异常时不能持有Threads_lock。示例:在构造异常时,可能需要获取heap_lock。
  bool throw_illegal_thread_state = false;

  // 我们必须释放Threads_lock才能在Thread::start中post jvmti事件
  {
	// 确保c++ Thread和OSThread结构体在操作之前没有被释放
    MutexLocker mu(Threads_lock);

	//从JDK 5开始threadStatus用于防止重新启动一个已经启动的线程,所以我们通常会发现javthread是null。然而,对于JNI附加的线程,在创建的线程对象(带有javthread集)和更新其线程状态之间有一个小窗口,因此我们必须检查这一点
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      //我们也可以检查stillborn标志来查看这个线程是否已经停止,但是由于历史原因,我们让线程在开始运行时检测它自己
      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
	  //分配c++线程结构并创建原生线程。
      //从java检索到的堆栈大小是有符号的,但是构造函数接受size_t(无符号类型),因此避免传递负值,因为这会导致非常大的堆栈。	
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz);
      // 此时可能由于缺少内存而没有为javthread创建osthread。检查这种情况并在必要时抛出异常。
      // 最终,我们可能想要更改这一点,以便只在线程成功创建时才获取锁——然后我们也可以执行这个检查并在javthread构造函数中抛出异常。
      if (native_thread->osthread() != NULL) {
        // 注意:当前线程没有在“prepare”中使用。
        native_thread->prepare(jthread);
      }
    }
  }

  if (throw_illegal_thread_state) {
    THROW(vmSymbols::java_lang_IllegalThreadStateException());
  }

  assert(native_thread != NULL, "Starting null thread?");

  if (native_thread->osthread() == NULL) {
    // No one should hold a reference to the 'native_thread'.
    delete native_thread;
    if (JvmtiExport::should_post_resource_exhausted()) {
      JvmtiExport::post_resource_exhausted(
        JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,
        "unable to create new native thread");
    }
    THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),
              "unable to create new native thread");
  }

  Thread::start(native_thread);

JVM_END

7.3 Thread yield

C- Thread
	M- yield() : 可以看到 ,  yield 同样是一个 native 方法
    
// Step 1:  \openjdk\hotspot\src\share\vm\prims\jvm.cpp
// 主要是2句 :  os::sleep(thread, MinSleepInterval, false);
// os::yield();
JVM_ENTRY(void, JVM_Yield(JNIEnv *env, jclass threadClass))
  JVMWrapper("JVM_Yield");
  if (os::dont_yield()) return;
#ifndef USDT2
  HS_DTRACE_PROBE0(hotspot, thread__yield);
#else /* USDT2 */
  HOTSPOT_THREAD_YIELD();
#endif /* USDT2 */
  // 当ConvertYieldToSleep关闭(默认)时,这与传统VM的yield使用相匹配。对于类似的线程行为至关重要
  if (ConvertYieldToSleep) {
    os::sleep(thread, MinSleepInterval, false);
  } else {
    os::yield();
  }
JVM_END
    
    
// TODO : 主要的其实还没有看懂 , 毕竟 C基础有限
	

总结

很多东西还是没完全说清楚 , 后面还得继续完善

更新记录

  • 20210727 : 完善代码 , 优化格局

致谢

芋道源码 : http://www.iocoder.cn/JUC/sike/aqs-3/

https://mp.weixin.qq.com/s?__biz=MzIxOTI1NTk5Nw==&mid=2650047475&idx=1&sn=4349ee6ac5e882c536238ed1237e5ba2&chksm=8fde2621b8a9af37df7d0c0a7ef3178d0253abf1e76a682134fce2f2218c93337c7de57835b7&scene=21#wechat_redirect

https://blog.csdn.net/javazejian/article/details/70768369

死磕系列 , 我的多线程导师
http://cmsblogs.com/?cat=151

// JVM 源码 C
https://www.jianshu.com/p/a604f1a9f875