对于程序的运行过程,操作系统中最重要的两个概念是进程和CPU,进程就是运行程序的一个抽象,CPU主要工作就是对进程的调度。需要理解的是,一个CPU在一个瞬间,只能执行一个进程,通常这个时间片段是几十毫秒或几百毫秒,但对于用户来讲,就像多个程序同时运行,这就是伪并行(对于一个CPU来讲)。进程包含几乎程序运行的所需要的所有信息,包括程序计数器、堆栈指针、程序对应地址空间(存放可执行程序、程序的数据、程序的堆栈等)的读写操作以及其他资源的信息。进程的执行有三个状态:正在运行的进程是运行态,还包括就绪态(可运行,CPU正在执行别的进程)、阻塞态(等待某个资源或某个事件发生之前的进程的状态)。三种状态的切换如下图所示:
为了实现进程模型,操作系统维护着一张表格,即进程表。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,如下图:
在了解进程表之后,还需要理解中断的概念:对于进程来说,中断意味着进程需要让出CPU,进程进入阻塞状态,需要在进程表项中保存进程相关信息,以便下次CPU执行时,可以继续执行进程;对于磁盘来说,中断是指磁盘完成了程序指定的响应的任务,产生的中断信号;对于CPU来说,在就绪队列中,轮询到下一个进程时间片时,从中断向量中读取到寄存器信息,将会继续执行该进程。
那什么是线程呢?为什么有了进程还需要线程呢?
首先,考虑程序的功能,往往不是单一的功能,比如在执行一件事的同时,可以进行其他事情,这时一方面,进程的创建相比于线程的创建来说,比较消耗资源,也就是线程更加轻量级;另一方面,线程可以共享地址空间,这对于一些应用程序来说,确实是比较需要的。然后,如果CPU密集型的任务,涉及到CPU的计算和上下文切换,多线程的处理能力,可能并不会比多进程有太大的优势,但对于I/O密集型的任务来说,而随着多核计算机的普及,硬件领先软件的情景出现,使得并行处理有了发展硬件支撑,所以多线程技术也得到了很好的发展。当然,线程也有缺点,由于一个线程死掉了意味着整个进程就死掉了,而一个进程死掉不会影响其他进程,所以多线程应用相对于进程应用来说,没有其表现的稳定。
线程包含各自的程序计数器、局部变量、堆栈以及对对共享空间的访问。
线程的生命周期包含5种类型(同进程一致):
线程间的状态切换如下图所示:
操作系统线程的实现有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虚拟机的配置和系统限制所影响:
一般线程数量的计算公式: