java线程-线程概述

157 阅读9分钟

我们在平时编程开发过程中,大概都在写crud,很少涉及到多线程编程,但是我们经常使用的框架,中间件,容器等都会使用到多线程,例如Tomcat就会使用多线程模型来处理请求。所以我们也会间接使用到多线程,如果对线程的技术有所了解,也能够让我们的编程技能有所提升,对我们阅读源码也会有所帮助。

本文主要了解一下线程的基本概念,比如线程的由来,线程的调度策略,线程的生命周期,线程模型,java线程的实现方式,多线程编程bug源头等。

线程由来

线程并非在计算机诞生之初就已经存在的,操作系统为了不断改善计算机系统性能和资源利用率,在这过程中发展出了进程,线程。接下来我们介绍一下进程和线程。

进程

进程是指计算机中已运⾏行行的程序。进程为曾经是分时系统的基本运作单位。在面向进程设计的系统中,进程是程序的基本执行实体;程序本身只是指令、数据及其组织形式的描述,进程才是程序的真正运行实例。

进程又分为单进程和多进程。

单进程

早期计算机只支持单进程,一次只能运行一个程序。程序只能按照串行的方式来运行,运行完上一个程序,才能运行下一个程序。每个程序在运行的过程中都是独占计算机资源的(CPU,内存)。

多进程

后面个人计算机的诞生,用户的需求是一边听音乐一边写文档,如果只是单进程,那就不能满足这样的需求,所以多个进程并行。早期计算机都是单核的,多个程序运行在一个cpu上,计算机内部采用并发的方式,实现多进程并行。在粗时间粒度上,多个进程看似并行,在细时间粒度上,两个程序实则交替运行。

并行并发.svg

从上图可以看出,并发的时候,会频繁的发生进程间的切换,切换的时候,进程会频繁的暂停,重启,在暂停的时候,进程会记录运行时环境,重启的时候,会恢复到当时的运行时环境。

操作系统用进程表(Process Table)来记录进程的执行信息,进程表中每一个表项叫做进程控制块(Process Control Block)。

  • 进程ID
  • 进程状态:NEW,READY,RUNNING,WAITING,TERMINATED。
  • 程序计数器
  • 寄存器
  • 调度信息
  • 文件列表
  • 其他信息

线程

是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

实际上,多进程已经满足了多个程序并行的需求,线程的引入是为了在设计,性能,易用性上做了进一步的提升。

在设计方面,引入线程后,就对进程进行了拆分管理,进程只负责记录共享资源的记录(虚拟内存中的代码段,数据段,文件等)。线程负责代码的执行,负责记录栈,程序计数器,寄存器等信息。操作系统会按照线程来分配CPU,原来的进程切换也替换为线程切换。

在性能方面,随着多核计算机的发展,多线程技术可以让一个程序并行运行在多个CPU上,程序的执行效率更高了。

在易用方面,每个线程负责执行一个逻辑,多个逻辑的调度执行,由操作系统完成。如果没有多线程技术,需要程序自身去维护,开发难度上升。

调度策略

对于支持多线程的操作系统,多个线程需要同时竞争同一个CPU来运行,操作系统就需要设计一套算法来调度线程运行。

常见的调度策略有:先来先服务,最短作业优先,最小最大公平算法,多级反馈队列,时间片轮询调度等。大部分操作系统采用的是时间片轮询调度算法,基于此算法的基础上组合其他算法,以兼容公平性,优先级,响应时间,吞吐量等。

时间片轮询算法:所有就绪线程,会放入一个队列中,操作系统每次都从队首取一个线程来分配时间片执行,当时间片用完了,操作系统会将这个线程暂停,放置队尾,然后再从队首取新的线程来执行,以此类推。当然除了时间片用完以后,还存在其他情况也会导致线程切换,比如等待线程IO读写完成,线程等待获取锁,线程主动让出时间片(yield函数)。

线程的生命周期

通用的线程生命周期

通用线程状态

线程在调度执行过程中,会经历很多状态,一般来说操作系统的线程生命周期包括:

  • **初始状态(NEW):**线程已经被创建,但是还不允许分配CPU执行。这个状态属于编程语言特有的,不过这里的所谓的被创建,仅仅是编程语言层面被创建,而在操作系统层面,真正的线程并没有被创建。
  • **可运行状态(READTY):**指的是线程可以分配CPU执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配CPU执行。
  • 当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了运行状态(RUNNING)
  • 运行状态的线程如果调用一个阻塞的API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态(WAITING),同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
  • 线程执行完或者出现异常就会进入终止状态(TERMINATED),终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。

通用线程状态转移

通用线程状态转移.svg

java中线程的生命周期

java线程状态

/**
* Thread的内部类
*/
public enum State {
        NEW,//初始化状态
        RUNNABLE,//可运行/运行状态
        BLOCKED,// 阻塞状态
        WAITING,// 无时限等待
        TIMED_WAITING,// 有时限等待
        TERMINATED;// 终止状态
}

其中BLOCKED,WAITING,TIMED_WAITING是一种状态,就是我们前面提到的休眠状态。只要java线程处于这三种状态之一,那么这个线程就永远没有CPU使用权。

java线程状态转移

java线程状态转移.svg

其中需要说明的是调用Thread.join(),其中join()是一种线程同步方法,例如有一个线程A,当调用A.join()的时候,执行这条语句的线程会等待A执行完,而等待中的这个线程,其状态会从RUNNABLE到WAITING/TIMED_WAITING。当A执行完,原来等待它的线程又会从WAITING/TIMED_WAITING->RUNNABLE

线程模型

所谓线程模型,其实就是线程的实现方式,常见的实现方式有:内核线程(1:1线程模型),用户线程(M:1线程模型),混合线程(M:N线程模型),不同的线程模型主要区别在于线程调度是由谁来完成的,是操作系统内核还是虚拟机。

内核线程

由操作系统内核来负责多线程调度的多线程实现方式就是内核线程。调度程序运行在内核空间,CPU处于内核态,所以内核线程,也叫做内核空间线程或者内核态线程。

内核线程是1:1线程模型,1个用户线程(应用程序开发者眼里的线程)对应1个内核线程。java线程就是采用这种线程模型。

1-1线程模型.svg

用户线程

用户线程指的是线程的调度由虚拟机完成,因为虚拟机本质上就是一个应用程序,运行在用户空间,所以,用户线程也叫做用户空间线程或者用户态线程。

用户线程的调度程序的实现思路跟内核线程的调度程序的实现思路基本一致。

用户线程是M:1线程模型,M个用户线程对应1个内核线程,该模型的线程管理是由用户空间的线程库来完成的,因此效率更高,并且高效的上下文切换和几乎无限制的线程数量.不过,如果一个线程执行阻塞系统调用,那么整个进程将会阻塞.再者,因为任一时间只有一个线程可以访问内核,所以多个用户线程不能并行运行在多核系统上。

M-1线程模型.svg

混合线程

用户线程虽然可以避免使用内核线程导致的内核态和用户态的上下文切换,但多个用户线程并不能并行在多核上,为了解决这个问题,计算机科学家发明了M:N线程模型,即混合线程。

M个用户线程对应N个内核线程,使得库和操作系统都可以管理线程,用户线程由运行时库调度器管理,内核线程由操作系统调度器管理,可运行的用户线程由运行时库分派并标记为准备好执行的可用线程,操作系统选择用户线程并将它映射到可用内核线程.

M-N线程模型.svg

java线程的实现方式

java实现线程的两种方式,一种叫Green Thread,另一种叫Native Thread。

Green Thread就是用户线程,即M:1线程模型,是JDK1.1,1.3版本实现的方式。现在使用的实现方式是Native Thread,就是内核线程,即1:1线程模型。