低并发编程战略上轻视技术,战术上重视技术闪:小宇,你怎么了,我看你脸色很差。小宇:今天去面试了,面试官问我Java线程的现状和改造。Shanke:哦,这是一个很常见的面试问题。不是有状态流程图吗?小宇:我知道,但是每次面试的时候,脑子里记得的流程图就变成这样了。闪退:哈哈哈。小鱼:你还在笑,我气死了,你能给我说说这些乱七八糟的状态吗?Flasher:没问题,还是老规矩,你先把所有状态都忘了,我从头说起!小宇:好。线程状态的本质首先你要明白,当我们谈论线程的状态时,我们在谈论什么?没错,它只是一个变量的值。哪个变量?Thread类中的一个变量叫做privatevolatileintthreadStatus=0;thisvalue是一个整数,不方便理解,可以通过映射关系(VM.toThreadState)转化为一个枚举类。publicenumState{NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;}所以,我们只盯着threadStatus值的变化。就这么简单。NEW现在我们还没有任何Thread类对象,因此不存在线程状态这样的东西。一切的起点都是从创建Thread类的对象开始的。线程t=新线程();当然,你以后可以带很多参数。Threadt=newThread(r,"name1");您还可以创建一个继承Thread类的新子类。线程t=newMyThread();你说线程池怎么会有没有new的线程?房子的内部也很新。}}publicclassExecutors{staticclassDefaultThreadFactoryimplementsThreadFactory{publicThreadnewThread(Runnabler){Threadt=newThread();返回吨;}}publicclassExecutors{staticclassDefaultThreadFactoryimplementsThreadFactory{publicThreadnewThread(Runnabler){Threadt=newThread();返回吨;}}}总是,一切的开始,都必须调用Thread类的构造函数。而这个构造方法最终会调用Thread类的init()方法。privatevoidinit(ThreadGroupg,Runnabletarget,Stringname,longstackSize,AccessControlContextacc,booleaninheritThreadLocals){this.grout=g;this.name=名称;tid=nextThreadID();}这个init方法只是针对Thread类对象中的Attributes,附加值,其他什么都不做。它不会重新分配theadStatus,因此它的值仍然是默认值。而这个值对应的状态就是STATE.NEW,如果一定要翻译成中文,就叫做初始状态。所以说了这么多,其实分析就是新建一个Thread类对象就是新建一个线程。这个时候这个线程的状态是NEW(初始状态)。在后面的分析中,会弱化threadStatus的整数值,直接改变线程状态。大家都知道,它只是改变了threadStatus的值。RUNNABLE你说,处于NEW状态的线程在操作系统中对应的是什么状态?乍一看,你没有仔细看我上面的分析。线程t=新线程();只是做了一些肤浅的工作,在Java语言级别将值附加到它自己的对象之一的属性,在操作系统级别什么也没做。所以这个NEW状态,不管是深还是浅,真的只是一个无聊的枚举值。下面,精彩的故事才刚刚开始。在调用start()方法之前,闲置在堆内存中的Thread对象不会活跃起来。t.开始();这个方法一旦被调用就惨了,最终会被调用成烦人的native方法。私有原生voidstart0();貌似改变状态不是threadStatus=xxx那么简单,但是有局部方法可以修改。九弯十八弯跟进jvm源码后,调用了这个方法。热点/src/os/linux/vm/os_linux.cpppthread_create();著名的unix创建线程的方法,pthread_create。这时,在操作系统内核中,创建了一个真正的线程。在Linux操作系统中,不存在刚刚创建但未启动的线程,创建后立即开始运行。虽然从源码中无法发现线程状态的变化,但是通过调试可以看到,在调用了Thread.start()方法之后,线程的状态变成了RUNNABLE,即运行状态。那么我们的状态图又丰富了。通过这一部分,我们知道了以下几点:1、Java调用start()后,操作系统中实际出现了一个线程,并立即运行。2、Java中的线程与操作系统内核中的线程是一对一的关系。3、调用start后,线程状态变为RUNNABLE,这是native方法中的一些代码导致的。RUNNING和READYCPU是一个核心,一次只能运行一个线程。具体执行哪个线程取决于操作系统的调度机制。所以,上面的RUNNABLE状态,准确的说,就是随时有机会准备运行的状态。这个状态的线程也分为运行在CPU中的线程,和一堆准备好等待CPU分配时间片运行的线程。就绪的线程会被存放在一个就绪队列中,等待被操作系统的调度机制选中,进入CPU运行。当然需要注意的是,这里的RUNNING和READY状态是我们自己创建的,为了描述方便。Java语言和操作系统都不区分这两种状态,在Java中都称为RUNNABLE。TERMINATED当线程完成执行(或调用已弃用的停止方法)时,线程的状态变为TERMINATED。此时线程无法复活。如果此时强行启动方法,会报错。java.lang.IllegalThreadStateException很简单,因为start方法的第一行就写的这么直接。publicsynchronizedvoidstart(){if(threadStatus!=0)thrownewIllegalThreadStateException();...}咦,如果这个时候强行把threadStatus改成0会怎么样呢?你可以尝试一下。BLOCKED完成了上面最常见最简单的线程生命周期。Init--Run--没有任何问题地终止。接下来,为了变得更复杂一点,我们让线程遇到一些障碍。首先创建对象锁。publicstaticfinalObjectlock=newObject();一个线程执行一个sychronized块,锁对象是lock,一直持有锁。newThread(()-{synchronized(lock){while(true){}}}).start();另一个线程也执行一个同步块,它的锁对象是lock。newThread(()-{synchronized(lock){...}}).start();那么,当进入synchronized块时,由于无法获取到锁,线程状态就会变成BLOCKED。同步方法也是如此。当线程获得锁后,就可以进入synchronized块,此时线程状态变为RUNNABLE。于是得到如下变换关系。当然,这只是线程状态的改变,线程也发生了一些实质性的变化。我们不考虑对同步的虚拟机进行极端优化。当进入一个synchronized块或方法而无法获得锁时,线程会进入一个锁对象的同步队列。当持有锁的线程释放锁时,会唤醒锁对象同步队列中的所有线程,这些线程会继续尝试抢锁。如此来回。比如有一个锁对象A,此时线程1持有锁。线程2、3、4分别抢到这个锁失败。线程1释放锁,线程2、3、4又变成RUNNABLE,继续抢锁。如果此时线程3抢到了锁。如此来回。WAITING部分最为复杂,也是面试中考验最多的部分。它将分为三个部分。听完我,你会发现这三个部分有很多相似之处,但它们不再是孤立的知识点。wait/notify我们刚才在synchronized块中添加了一些东西。newThread(()-{synchronized(lock){...lock.wait();...}}).start();一旦调用lock.wait()方法,就会发生三件事。1.释放锁对象锁(暗示必须先获取锁)2.线程状态变为WAITING3.当线程进入锁对象的等待队列,当线程被唤醒时,从队列中移除等待队列并从WAITING状态中取出返回RUNNABLE状态怎么办?同一个对象的notify/notifyAll方法必须被另一个线程调用。newThread(()-{synchronized(lock){...lock.notify();...}}).start();只是notify只唤醒一个线程,而notifyAll唤醒所有等待队列的线程。但是需要注意的是,被唤醒的线程从等待队列中移除,状态变为RUNNABLE,但仍然需要抢锁。只有抢锁成功后,才能从wait方法中返回,继续执行。如果失败,则与上一部分的BLOCKED流程相同。所以我们的整个流程图现在看起来像这样。join主线程是这样写的。publicstaticvoidmain(String[]args){threadt=newThread();t.开始();t.join();}当t.join()被执行时,主线程会变成WAITING状态,直到线程t执行完毕,主线程会回到RUNNABLE状态继续执行。看起来主线程正在执行时另一个线程正在加入队列,并且主线程在完成之前不会继续。因此,我们的状态图多了两项。那么join是如何神奇地实现这一切的呢?是不是也和wait一样放在等待队列中?打开Thread.join()的源码,你会发现它非常简单。//Thread.java//无参join的有用信息就是这个,省去了多余的分支publicsynchronizedvoidjoin(){while(isAlive()){wait();}}也就是说,它的本质还是执行了wait()方法,锁对象就是Threadt对象本身。然后从RUNNABLE到WAITING,和执行wait()方法是一模一样的。那么如何从WAITING返回到RUNNABLE呢?主线程调用wait,需要另外一个线程通知。我们需要这个子线程t在它结束之前调用t.notifyAll()吗?答案是否定的,那么只有一种可能,线程t结束后,jvm会自动调用t.notifyAll(),而我们的程序不会显示出来。没错,就是这样。如何证明这一点?道听途说还不够,今天得脱下jvm的外衣。果然找到了下面的代码。hotspot/src/share/vm/runtime/thread.cppvoidJavaThread::exit(...){...ensure_join(this);...}staticvoidensure_join(JavaThread*thread){...锁定。通知所有(线程);...}我们可以看到虚拟机执行完一个thread的方法后执行了一个ensure_join方法。看名字就知道是专为join设计的。继续往下看,会发现一个关键代码,lock.notify_all,就是一个线程结束后,会自动调用自己的notifyAll方法的证明。所以,其实join就是wait,线程结束就是notifyAll。现在,是不是更清楚了。park/unpark上面有wait和notify的机制,下面就很容易理解了。线程调用以下方法。LockSupport.park()线程状态会从RUNNABLE变为WAITING,另一个线程调用LockSupport.unpark(Threadjustthread)刚才线程会从WAITING返回到RUNNABLE但是从线程状态流的角度来说,和wait是一样的并通知。从实现机制来看,更简单。1.park和unpark不需要提前获取锁,或者与锁无关。2.没有等待队列,unpark会准确唤醒某个线程。3、park和unpark没有顺序要求。您可以先调用unpark。关于第三点,涉及公园原则。这里我只简单说明一下。线程有一个计数器,初始值为0。调用park意味着如果值为0,线程将被挂起,状态变为WAITING。如果此值为1,则将此值更改为0,并且不执行任何其他操作。调用unpark就是把这个值改成1。那我用三个例子,你就基本明白了。//示例1LockSupport.unpark(Thread.currentThread());//1LockSupport.park();//0System.out.println("Youcanrunhere");//Example2LockSupport.unpark(Thread.currentThread());//1LockSupport.unpark(Thread.currentThread());//1LockSupport.park();//0System.out.println("Youcanrunhere");//Example3LockSupport.unpark(Thread.currentThread());//1LockSupport.unpark(Thread.currentThread());//1LockSupport.park();//0LockSupport.park();//WAITINGSystem.out.println("这里不能运行");park的使用很简单,也是JDK中锁实现的底层。它在JVM和操作系统层面的原理非常复杂,改天找个专栏来讲解。现在我们的状态图可以再次更新了。TIMED_WAITING部分再简单不过了。在上述导致线程变为WAITING状态的方法中加入超时参数,就变成了将线程变为TIMED_WAITING状态的方法。我们直接更新流程图。这些方法唯一的区别在于,从TIMED_WAITING返回RUNNABLE不仅可以通过前面的方法,超时后也可以返回到RUNNABLE状态。就这样。还有,大家看。wait需要先获取锁,然后释放锁,然后等待通知。join是对wait的封装。Park需要等待unpark唤醒,或者提前被unpark下发唤醒权限。有没有一种方法可以挂起线程并仅通过等待超时到期将其唤醒。这个方法就是Thread.sleep(long)。我们将它添加到图片中,这部分就完成了。然后将其添加到全局图中。后记Java线程状态,有6种NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED和经典的线程五态模型,有5种状态创建准备执行阻塞终止不同的实现者,有合并和拆分的可能。例如Java将五态模型中的就绪和执行统一为RUNNABLE,将阻塞(即无法获得CPU运行机会的状态)细分为BLOCKED、WAITING、TIMED_WAITING。这里不评价好坏。也就是说,在BLOCKED、WAITING、TIMED_WAITING三种状态下,线程是不可能获得CPU运行权的。您可以将其称为挂起、阻止、睡眠或等待。你还会在许多文章中看到这些词来回使用不太认真。这里还有两个问题可能会让你感到困惑。在jdk的Lock接口中调用锁。如果得不到锁,线程就会挂起。此时线程的状态是什么?有多少同学认为应该是BLOCKED状态,和synchronized不能获取锁的效果是一样的?但是仔细看我上面的文章,有一句话提到jdk中lock的实现是基于AQS的,而AQS底层是使用park和unpark来挂起和唤醒线程的,所以应该改成WAITING或TIMED_WAITING状态。调用阻塞IO方法时线程是什么状态?比如在socket编程时,调用accept()、read()等阻塞方法时,线程处于什么状态?答案是处于RUNNABLE状态,但实际上这个线程是无法获得运行权的,因为它在操作系统层面处于阻塞状态,需要等到IO就绪后才能变为准备好。但是在Java层面,JVM认为等待IO等同于等待CPU执行权。人们是这么认为的。我这里还是不讨论它的优缺点。如果你觉得不舒服,你可以自己设计一种语言。那你不管你怎么想,没人能拿你怎么样。比如我要设计一门语言,我认为CPU可以调度执行的线程都是死状态。这样我的语文一定会有一道经典的面试题。为什么山克会把一个可运行的线程定义为死状态呢?好了,今天的文章就到这里。这篇文章写的有点投入,写到这里的时候发现小鱼忘记了开头。
