美文网首页工作生活
Java知识总结之Thread

Java知识总结之Thread

作者: WangGavin | 来源:发表于2019-06-30 14:25 被阅读0次

    1 线程的生命周期

    每个线程都有自己的局部变量表、程序计数器以及生命周期。

    线程生命周期状态图

    上图就时一个线程的生命周期图,答题可以分为5个主要阶段:

    • NEW
    • RUNNABLE
    • RUNNING
    • BLOCKED
    • TERMINATED

    1.1 NEW状态

    当我们new一个Thread对象时,此时它并不处于执行状态,因为没有调用start方法,那么线程的状态为NEW状态。

    NEW状态通过start方法进入RUNNABLE状态。

    1.2 RUNNABLE状态

    线程对象进入RUNNABLE状态必须调用start方法,那么此时才是真正地在JVM进程中创建了一个线程。

    线程一启动就可以立即得到执行吗?

    不一定,线程的运行与否和进程一样都要听令于CPU的调度,那么我们把这个中间状态称为可执行状态(RUNNABLE),也即是说它具备执行的资格,但是并没有真正地执行起来二十在等待CPU的调度。

    严格意义上讲,RUNNABLE的线程只能意外终止或者进入RUNNING状体。

    1.3 RUNNING状态

    一旦CPU通过轮询或者其它方式从任务可执行队列中选中了线程,那么此时它才能真正地执行自己地逻辑代码,需要说明的一点是一个正在RUNNING状态的线程事实上也是RUNNABLE的,但是反过来则不成立。

    在该状态下,线程的状态可以发生如下的状态转换:

    • 直接进入TERMINATED状态,比如调用已不推荐的stop方法或者是判断某个逻辑标志
    • 进入BLOCKED状态,比如调用了sleep或者wait方法而加入了waitSet中
    • 进行某个阻塞的IO操作,比如因网络数据的读写进入了BLOCKED状态
    • 获取某个锁资源,从而加入到该锁的阻塞队列中进入了BLOCKED状态。
    • 由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE状态。
    • 线程主动调用yield方法,放弃CPU执行权,进入RUNNABLE状态

    1.4 BLOCKED状态

    线程在BLOCKED状态中可以切换如下几个状态:

    • 直接进入TERMINATED状态,比如调用JDK不推荐使用的stop方法或者意外死亡(JVM Crash)
    • 线程阻塞的操作结束,比如读取了想要的数据字节进入到RUNNABLE状态
    • 线程完成了指定时间的休眠,进入到了RUNNABLE状态
    • wait中的线程被其它线程notify/notifyAll唤醒,进入RUNNABLE状态
    • 线程获取到了某个锁资源,进入RUNNABLE状态
    • 线程在阻塞过程中被打断,比如其它线程调用 interrupt方法,进入RUNNABLE状态。

    1.5 TERMINATED状态

    TERMINATED是一个线程的最终状态,在该状态中线程将不会切换到其它任何状态,线程进入TERMINATED状态,意味着该线程的整个生命周期都结束了,下列情况下将会使线程进入TERMINATED状态。

    • 线程运行正常结束,结束生命周期
    • 线程运行出错意外结束
    • JVM Crash,导致所有的线程都结束。

    2 线程的start方法

    可以从源码知道,start方法首先会判断线程状态,如过不在符合条件的状态会抛异常,然后会调用start0()一个jni方法,然后jni方法会调用Thread的run方法,从而使run方法里面的逻辑业务可以执行。

    • Thread被构造后处于New状态,实际上它的内部属性为0.
    • 不能两次启动Thread,否则会抛出IlleagalThreadStateException
    • 线程启动后将被加入到一个ThreadGroup中
    • 线程处于TERMINAL状态,是无法回到RUNNABLE/RUNNING状态的。

    2.1 模板设计模式在Thread中的应用

    线程真正执行的逻辑实在run方法里,通常我们会把run方法成为线程的执行单元。其实Thread里的start和run方法就是一个比较典型的模板设计模式,父类编写算法数据结果,子类负责实现具体细节,另外Runnalbe接口的使用也可以将线程的控制和业务逻辑的执行彻底分开来。

    2.2 Runnable接口

    创建线程只有通过构造方法构建,而实现线程具体的业务执行单元有两种,一种是重写run方法,另一种是实现Runnable接口里的run方法,并且将Runnable的实例传给Thread构造方法当参数。

    3 Thread构造方法

    3.1 线程的命名

    如果没有显式地为线程指定名字,线程会以Thread-作为前缀和一个自增数字进行组合,这个自增数字会不断在JVM中进行自增。

    3.2 命名线程

    强烈建议构造线程时自定义线程名,通常都是自定义前缀加数字命名。

    注意:线程为未启动之前可以通过setName()方法更改线程名,如果已启动,则无法更改。

    3.3 线程的父子关系

    • 一个线程的创建肯定是在另一个线程完成的
    • 被创建线程的父线程就是创建它的线程

    3.4 Thread和ThreadGroup

    从源码可以看出,如果在构造器未指定线程组,那么该线程会默认加入到父线程所在的线程组中。

    • main线程所在的线程组叫main
    • 构造一个线程时如果没有显示指定一个ThreadGroup,那么它将会和父线程同属于一个ThreadGroup

    3.5 Thread和Runnable

    Thread负责线程本身的职责和控制,Runnable负责逻辑执行单元

    3.6 Thread和JVM虚拟机栈

    在构造方法中,我们可以发现一个stackSize参数。一般情况下,创建线程的时候不会手动指定栈内存空间的地址空间字节数组,同意用xss参数设置即可,在某些平台下,越高的stack设定,可以允许的递归深度越高;反之,越少的stack设定,则递归深度越浅。不过这个参数很依赖平台。

    3.7 JVM内存结构

    jvm内存结构图

    3.7.1 程序计数器

    无论任何语言,其实最终都是需要操作系统通过控制总线向CPU发送机器指令,Java也不例外,程序计数器在JVM中所起的作用就是用于存放当前线程接下来将要执行的字节码指令、分支、循环、跳转、异常处理等信息。在任何时候,一个处理器只处理其中一个线程中的指令,为了能够在CPU时间片轮转切换上下文顺利回到正确的执行位置,每个线程都需要具有一个独立的程序计数器,各个线程之间互不影响,因此JVM将此块内存区域设计了线程私有。

    3.7.2 java虚拟机栈

    与程序计数器内存一样,Java虚拟机栈也是线程私有的,它的生命周期和线程一样,也是在jvm运行时创建的,在线程中,方法在执行的时候会在创建一个叫栈帧的数据结构,主要用于存放方法的局部变量表,操作栈,动态链接,方法出口等信息。

    每一个线程在创建的时候,jvm都会为其创建对应的虚拟机栈,虚拟机栈的大小可以用-xss来配置,方法的调用和结束就是栈帧压入和弹出的过程,同等的虚拟机栈如果局部变量表等占用内存越小被压入的栈帧就越多,反之则压入的栈帧越少,一般将栈帧的内存大小称为宽度,而栈帧的数量称为虚拟机栈的深度。

    一个虚拟机栈对应一个线程,当前CPU调度的那个线程叫活动线程;一个栈帧对应一个方法,或者线程的虚拟机栈的最顶部的栈帧代表了当前正在执行的方法,而这个栈帧也称“当前栈帧”。

    3.7.3 本地方法栈

    JVM为本地方法所划分的区域叫本地方法栈,这款区域内存自由度高,完全靠不同的JVM厂商来实现,Java虚拟机规范并未给出明确的规定,但它一样也是线程私有的内存区域。

    3.7.4 堆内存

    堆内存是jvm中最大的一块内存区域,被所有的线程所共享,Java在运行期间所创建的对象几乎所有都存放在此区域,该区域也是垃圾回收期重点照顾的区域,因此堆也称为“GC堆”。

    堆内存一般分为新生代和老年代。

    3.7.5 方法区

    方法区也是被多个线程所共享的内存区域,它主要用于存放已经被虚拟机加载的类信息、常量、静态变量、即时编辑器(JIT)编译后的代码等数据。

    虽然在Java虚拟机规范中,方法区被划为堆内存的一个分区,但是它还是经常称为“非堆”,有时候也称为“持久代”

    3.7.6 java8元空间

    自JDK1.8后,JVM的内存区域发生了一些改变,实际上是持久代内存被彻底删除,取而代之的元空间。

    元空间同样是堆内存的一部分,JVM为每个类加载器分配一块内存块列表,进行线性分配,块的大小取决于类加载器的类型.sun/反射、代理对应的类加载器块会分配的小一些,之前的版本会单独卸载回收某个类,而现在是GC过程中发现某个类加载器已经具备回收的条件,则会将整个类加载器相关的元空间全部回收掉,这样可以减少内存碎片,节省GC扫描和压缩的时间。

    3.7.7 Thread与虚拟机栈

    JVM中,程序技术器是内存较小的一块,而且该部分内存不会出现任何溢出异常,与线程创建,运行,销毁等关系的是虚拟机栈内存,而且虚拟机栈内存划分的大小直接决定了一个JVM进程中可以创建多少个线程。

    我们可以粗略地认为:一个Java进程内存的大小:堆内存+线程数*虚拟机栈内存

    4 守护线程

    守护线程是一类比较特殊的线程,一般用于处理一些后台的任务,比如JDK的垃圾回收线程。

    如果JVM中没有一个非守护线程,则JVM的进程会退出。

    • 守护线程的设置非常简单:setDaemon(),true为守护线程,false为正常线程。
    • 线程是否为守护线程和它的父线程有很大关系,如果父线程是正常线程,则子线程也是正常线程,反之亦然,如果想修改可以用setDaemon()方法修改。isDaemon()方法可以判断该线程是不是守护线程。
    • setDaemon只能在线程启动之前设置,否则会抛出IlleagalStateException.

    守护线程通常用作执行一些后天任务,又是也称它为“后台线程”,当你希望关闭某些线程的时候,或者退出JVM进程的时候,一些线程能够关闭,此时可以考虑用守护线程完成这样的工作。

    5 线程Sleep

    sleep方法

    sleep是一个静态方法,一个需要传入毫秒数,另一个需要传入毫秒数和纳秒数。

    sleep会使当前线程进入指定毫秒数的休眠,暂停执行,虽然给定了休眠时间,但是最终要以系统的定时器和调度器精度为准。休眠有一个特性,就是它不会放弃其monitor锁的所有权。

    建议使用TimeUnit代替Thread.sleep,TimeUnit为sleep提供了简洁的封装。

    6 线程yield

    yield方法是一种启发式的方法,它会提醒调度器我愿意放弃当前的CPU资源,如果CPU资源不紧张,则会忽略此提醒。

    使用yield方法通常会使线程从RUNNING状态切换到RUNNABLE状态,一般这个方法不太常用。

    yield只是个提示,CPU调度器不能保证每次都能满足yield提示。

    6.1 sleep与yield

    yield方法实际上就是调用了sleep(0),但他们在本质上有很大差别。

    • sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗。
    • yield只是对CPU调度器的一个提示,如果CPU没有忽略这个提示,他会导致线程上下文的切换.
    • sleep会导致线程短暂的block,会在给定的时间内释放CPU资源.
    • yield会使处于RUNNING状态的Thread进入RUNNABLE状态.
    • sleep几乎能保证完成给定时间的休眠,而yield的提示不一定能保证.
    • 一个线程sleep,另一个线程调用interrupt会捕获到中断信号,而yield不会.

    7 线程优先级

    线程优先级相关方法

    进程有优先级,线程同样也有优先级,理论上较高的优先级会取得优先被CPU调度的机会,但是事实上也不是相当如愿的,设置线程优先级的操作也是一个hint操作

    • 对于root用户,他会hint操作系统想要你设置的优先级,否则它会忽略
    • 如果CPU比较忙,设置优先级可能会获得更多的时间片,但是闲时优先级的高低几乎不起任何作用.

    从设置线程优先级的源码可以知道,优先级不能小于1,也不能大于10,如果指定的优先级大于线程所在的线程组的优先级,那么会失效,取而代之的是group的最大优先级.

    一般情况下,不会对线程设置优先级,更不会让某些业务严重地以来线程的优先级,比如权重.

    线程默认的优先级和它的父线程保持一致,一般情况下都是5,因为main线程的优先级为5,所有由它派生的线程的优先级都为5.

    8 获取线程ID

    public long getID()获取线程的唯一ID,线程的ID在整个进程中都是唯一的,并且是从0开始递增.

    9 获取当前线程

    public static Thread currentThread()用于返回当前的执行线程的引用,这个方法虽然很简单,但是使用非常广泛.

    10 设置线程上下文类加载器

    public ClassLoader getContextClassLoader获取线程上下文的类加载器,就是说这个线程是由哪个类加载器加载的,如果没有修改上下文类加载器,那么类加载器默认就是保持与父线程一样的类加载器

    public void setContextClassLoader(ClassLoader cl) 这个方法可以打破Java类加载器的父委托机制,有时候该方法也称为Java类加载器的后门.

    11 线程Interrupt

    interrupt

    如下方法都可以让线程进入阻塞状态,而使用interrupt就可以打断阻塞.

    让线程进入阻塞状态的方法

    一个线程通过上述方法进入阻塞状态,若另一个线程调用了被阻塞线程的interrupt方法,则会打断这种阻塞,因此这种方法称为可中断方法.

    interrupt原理:在一个线程内部存在着名为interrupt flag的标识,如果一个线程被interrupt,那么它的flag将被设置;如果当前线程正在执行可中断方法被阻塞时,调用interrupt使其中断,反而会导致flag被清除(这个也不难理解,可中断方法捕获到了中断信号后,为了不影响线程中其他方法的执行,将线程的interrupt标识重置也是一种设计);如果线程已死,调用interrupt会被直接忽略.

    11.1 isInterrupted()

    isInterrupted()方法主要判断线程是否被中断,该方法仅仅是对flag标识的一个判断,并不会影响标识发生任何改变

    11.2 interrupted()

    interrupted()是一个静态方法,虽然它可以判断当前线程是否被中断,但是它和成员方法isInterrupted()有很大差别.调用该方法会直接擦除该线程的interrupt标识.

    需要注意的是,如果当前线程被打断了,那么第一次调用interrupted()时会返回true并擦除interrupt标识,第二次包括以后的调用就会返回false,除非此期间线程有一次地被打断.

    isInterrupted()和interrupted()两个方法实际调用的是

    image.png
    只是一个能擦除flag标识,一个没有

    12 线程join

    join方法

    join某个线程A,会使当前线程B进入等待,知道线程A结束生命周期,或者是到达指定时间,那么此段时间线程B是block的,而不是A线程.

    13 如何关闭一个线程

    13.1 正常关闭

    • 线程结束生命周期结束
    • 捕获中断信号关闭线程:
    • 使用volatile开关控制:由于线程的interrupt标识有可能会被擦除,或者逻辑单元中不会有任何可中断方法,所以使用volatile修饰的flag标识关闭线程也是一种常用做法.

    14 异常退出

    在一个线程的执行单元中,是不允许抛出checked异常的,不论Thread中的run方法,还是Runnable中的run方法,如果线程在运行过程中需要捕获checked异常并且需要判断是否还有运行下去的必要,那么此时可以将checked异常封装成unchecked异常(RuntimeException)抛出进而结束线程的生命周期

    15 进程假死

    所谓进程假死,就是进程虽然存在,但没有日志输出,程序不进行任何的作业,看起来像死了一样,但事实上是没有死的,程序之所以出现这样的情况,大多是某个线程阻塞了,或者线程出现了死锁的情况.

    相关文章

      网友评论

        本文标题:Java知识总结之Thread

        本文链接:https://www.haomeiwen.com/subject/zhmvcctx.html