线程与Java线程

对于程序的运行过程,操作系统中最重要的两个概念是进程和CPU,进程就是运行程序的一个抽象,CPU主要工作就是对进程的调度。需要理解的是,一个CPU在一个瞬间,只能执行一个进程,通常这个时间片段是几十毫秒或几百毫秒,但对于用户来讲,就像多个程序同时运行,这就是伪并行(对于一个CPU来讲)。进程包含几乎程序运行的所需要的所有信息,包括程序计数器、堆栈指针、程序对应地址空间(存放可执行程序、程序的数据、程序的堆栈等)的读写操作以及其他资源的信息。进程的执行有三个状态:正在运行的进程是运行态,还包括就绪态(可运行,CPU正在执行别的进程)、阻塞态(等待某个资源或某个事件发生之前的进程的状态)。三种状态的切换如下图所示:

为了实现进程模型,操作系统维护着一张表格,即进程表。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,如下图:

在了解进程表之后,还需要理解中断的概念:对于进程来说,中断意味着进程需要让出CPU,进程进入阻塞状态,需要在进程表项中保存进程相关信息,以便下次CPU执行时,可以继续执行进程;对于磁盘来说,中断是指磁盘完成了程序指定的响应的任务,产生的中断信号;对于CPU来说,在就绪队列中,轮询到下一个进程时间片时,从中断向量中读取到寄存器信息,将会继续执行该进程。

那什么是线程呢?为什么有了进程还需要线程呢?

首先,考虑程序的功能,往往不是单一的功能,比如在执行一件事的同时,可以进行其他事情,这时一方面,进程的创建相比于线程的创建来说,比较消耗资源,也就是线程更加轻量级;另一方面,线程可以共享地址空间,这对于一些应用程序来说,确实是比较需要的。然后,如果CPU密集型的任务,涉及到CPU的计算和上下文切换,多线程的处理能力,可能并不会比多进程有太大的优势,但对于I/O密集型的任务来说,而随着多核计算机的普及,硬件领先软件的情景出现,使得并行处理有了发展硬件支撑,所以多线程技术也得到了很好的发展。当然,线程也有缺点,由于一个线程死掉了意味着整个进程就死掉了,而一个进程死掉不会影响其他进程,所以多线程应用相对于进程应用来说,没有其表现的稳定。

线程包含各自的程序计数器、局部变量、堆栈以及对对共享空间的访问。

线程的生命周期包含5种类型(同进程一致):

  • NEW(新建):非系统中真实存在的线程状态
  • RUNNABLE(就绪):
  • RUNNING(运行):
  • BLOCKED(阻塞):
  • TERMINATED(终止):非系统中真实存在的线程状态

线程间的状态切换如下图所示:

操作系统线程的实现有3种方式:使用内核线程实现,使用用户线程实现,使用用户线程加轻量级进程混合实现。

Java线程是JVM进程的线程,由于多核系统的普及,充分发挥多核系统的调度优势,JVM较新版本所支持的所有平台上,大部分采用的是内核实现方式的线程模型。即通过轻量级进程接口(LWP)调用系统的内核线程KLT,再通过操作系统的调度器进行线程的分配执行。

Java线程的在JVM内存结构中包括私有空间和共有空间,也就是Java虚拟机的内存模型。根据虚拟机规范,Java线程私有的空间包括程序计数器,存放当前线程接下来要执行的字节码指令、分支、循环、跳转、异常处理等;Java虚拟机栈,生命周期与线程相同,在方法执行时都需要创建栈帧的数据结构,存放局部变量表、操作栈、动态链接、方法出口等,虚拟机栈大小通过-xss参数配置;本地方法栈,专门用来存储JNI方法的内存区域。Java线程共有的空间包括堆内存,用于存储Java运行时期创建的对象,垃圾回收大部分发生在此区域,堆内存还分新生代(Eden区、From Survivor区、To Survivor区)和老年代;方法区,主要存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等,在1.7版本JVM使用的是永久代,在1.8使用的是元空间来实现方法区。

在JDK中代表线程的是Thread类,Java Thread定义了线程名、线程ID、优先级、是否守护线程、执行目标、线程组、线程状态等属性。而Runnable接口是线程类的执行目标,通过模板设计模式将执行目标的run方法,封装到了线程Thread的start中。所以线程执行的方法是start(),而不是run()。由Thread类创建的对象都会一一映射到操作系统中的OSThread,Thread类通过一系列native方法(JNI)来进行线程的操作。

线程操作

线程sleep:当前线程进入指定时间的休眠(注:具体休眠时间以系统的调度的精度为准);

线程yield:主动放弃当前的CPU资源(有可能被CPU忽略),状态由Running->Runnable;

线程interrupt:打断进入阻塞状态(调用wait、sleep、join等)的线程,只是阻塞状态被打断,不等于结束线程的生命周期。

线程join:在线程A中,线程B调用join方法(可带时间参数),会使线程A进入等待,直到线程A结束生命周期或者超过指定的时间参数,在此期间线程B处于BLOCKED状态。

线程关闭:stop方法(已过期,不建议使用);正常关闭(线程结束生命周期正常结束;捕获中断信号关闭线程;使用volatile变量控制线程关闭);异常关闭(通过抛出异常退出线程;进程假死-线程阻塞或者死锁导致)

线程wait/notify/notifyAll:wait方法使线程进入等待状态,由Runnable变成Waiting;notify/notifyAll方法唤醒等待状态的线程。线程的sleep和wait看起来都是让线程进入等待状态,不过二者是有区别的,线程sleep之后,不会释放调monitor对象锁,只有当线程执行完成之后,其他线程才可以重新进入,而线程wait之后,当前线程会释放调monitor对象锁,其他线程可以进入同步块,线程唤醒之后再重新竞争锁。典型的wait/notify机制,比较适合生产者和消费者模式,如下所示:

在经典的生产、消费模型里,当线程在wait方法,被唤醒之后,不是直接进入Runnable转态,而是先进入阻塞队列,进行等待锁,也就是Blocked,获得锁之后才能进入Runnable状态。

线程状态

Java线程状态包括New(初始)、Runnable(运行状态-包含就绪和运行中)、Blocked(阻塞,阻塞于锁)、Waiting(等待,等待其他线程的动作-通知或中断)、Time_Waiting(超时等待)、Terminated(终止)。

线程调度

线程调度就是为某个线程分配CPU的使用权的过程,这个过程一般分为抢占式调度和协同式调度。Java线程属于抢占式调度,每个线程都会分同样的执行时间片,每次执行时候涉及到上下文切换。在JVM规范中规定每个线程都有优先级,优先级高优先执行,同样优先级则随机选择执行,但实际情况中,这并不是绝对的,所以不能严格按照优先级顺序编写逻辑。

线程数量

线程数量主要受到JVM虚拟机的配置和系统限制所影响:

  • JVM虚拟机配置
    • 堆内存
    • 栈空间
  • 系统限制
    • /proc/sys/kernel/threads-max
    • /proc/sys/kernel/pid_max
    • /proc/sys/vm/max_map_count
    • max_user_process(ulimit -u)

一般线程数量的计算公式:

  • 线程数量 =(最大地址空间(MaxProcessMemory) - JVM堆内存 - ReservedOsMemory(系统保留内存))/ThreadStackSize(Xss)
  • 线程数量 = 内核数量 / (1 - 堵塞率)

  1. 《Java并发编程的艺术》
  2. 《Java并发编程实战》
  3. 《现代操作系统-第四版》
  4. 《深入理解Java虚拟机》
  5. http://www.cnblogs.com/llguanli/p/7095457.html
  6. https://blog.csdn.net/yangmx_5/article/details/68065299
  7. https://blog.csdn.net/CringKong/article/details/79994511
  8. https://blog.csdn.net/ns_code/article/details/17225469
  9. https://www.zhihu.com/question/23096638
  10. https://juejin.im/post/5aea581ff265da0b82629c76

原文发布于微信公众号 - BanzClub(banz-club)

原文发表时间:2018-12-10

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券