目前,多线程编程可以说是在大部分平台和应用上都需要重视和实现的一个基本需求,是一个普遍性的现状。而多线程编程相关的概念以及在实现多线程编程的过程中会遇到的一些问题和难题,也是独立于开发者所面向的操作系统和开发语言的。所以虽然本系列关于多线程编程知识的文章面向的是 Java 平台,使用的例子大部分是用 Kotlin 语言实现的,但在概念上对于大部分开发者来说都是通用且可以理解的。希望本系列文章对你有所帮助。
进程、线程、任务、多线程编程
程序(Program)是对指令、数据及其组织形式的描述,是一种静态的概念。进程(Process)是程序的运行实例,每个被启动的程序就对应运行于操作系统上的一个进程,是一种动态的概念。进程是程序向操作系统申请资源(内存空间、文件句柄等)的基本单位,也是操作系统进行资源调度和资源分配的基本单位。运行一个 Java 程序实质上就是启动了一个 Java 虚拟机进程。
线程(Thread)是进程中可独立执行的最小单位,也是操作系统能够进行运算调度的最小单位。每个线程总是包含于特定的进程内,一个进程可以包含多个线程且至少包含一个线程,是进程中的实际运作单位。同一个进程中的所有线程共享该进程中的资源(内存空间、文件句柄等)。
线程所要完成的逻辑计算就被称为任务。特定的线程在创建之初的目的就是为了让其来执行特定的行为逻辑,其所要完成的工作就被称为该线程的任务。
多线程编程是一种以线程为基本抽象单位的编程范式(Praadigm)。所有的现代操作系统都支持多任务处理,多任务处理有两种不同的类型:基于进程的多任务处理和基于线程的多任务处理。基于进程的多任务处理指操作系统支持同时运行两个或更多个程序,进程是调度程序能够调度的最小代码单元。基于线程的多任务处理意味着单个进程可以同时执行多个任务,线程是调度程序能够调度的最小代码单元。基于线程的多任务处理需要的开销要比基于进程的多任务处理小得多。进程是重量级的任务,每个进程需要有自己的地址空间,进程间通信开销很大而且有很多限制,从一个进程上下文切换到另一个进程上下文的开销也很大。线程是轻量级的任务,它们共享同个进程下的资源,线程间通信的开销不大,并且同个进程下的不同线程上下文间的切换所需要的的开销要比不同进程上下文间的切换小得多。
基于进程的多任务处理是由操作系统来实现并管理的,一般的程序开发接触不到这个层面。而基于线程的多任务处理则可以由程序开发者自己来实现并进行管理。可以说,多线程编程的一个目的就是为了实现基于线程的多任务处理。
Java 对多线程编程提供了内置支持。Java 平台上的线程就对应于其 java.lang.Thread 类,我们可以简单地以创建多个 Thread 实例并启动来实现多线程编程。
Java 本身是一个多线程的平台,即使开发者没有主动创建线程,此时进程还是使用到了多个线程,例如还存在 GC 线程。所谓的单线程编程往往指的是在程序中开发者没有主动创建线程。
创建线程
Java 标准类库 java.lang.Thread 是 Java 平台对线程这个概念的抽象实现,Thread 类或者其子类的一个实例就是一个线程。每个线程在定义之初就是为了执行特定的任务,任务的处理逻辑可以直接声明在 Thread 类的 run() 方法内部或者间接通过该方法来进行调用。
Java 平台提供了两种不同的方式来让开发者声明线程所需要执行的任务:
实现 Runnable 接口
继承 Thread 类
实现 Runnable 接口
Runnable 接口抽象了一个可执行的代码单元,其本身仅包含一个抽象方法:
public interface Runnable {
public abstract void run();
}
可以依托任何实现了 Runnable 接口的对象来创建线程,即将实现了 Runnable 接口的对象作为构造参数来声明 Thread 对象。
从以下的输出结果可以看到,Task 的 run() 方法在非主线程的 Thread-0 上被执行。
/**
* 作者:leavesC
* 时间:2020/8/1 16:17
* 描述:
* GitHub:https://github.com/leavesC
*/
class Task : Runnable {
override fun run() {
for (i in 1..5) {
println("${Thread.currentThread().name} index: $i")
}
}
}
fun main() {
println("currentThread name: " + Thread.currentThread().name)
val thread = Thread(Task())
thread.start()
// currentThread name: main
// Thread-0 index: 1
// Thread-0 index: 2
// Thread-0 index: 3
// Thread-0 index: 4
// Thread-0 index: 5
}
继承 Thread 类
继承 Thread 类需要重写其 run() 方法,该方法是线程运行的入口点。
/**
* 作者:leavesC
* 时间:2020/8/1 16:17
* 描述:
* GitHub:https://github.com/leavesC
*/
class MyThread : Thread() {
override fun run() {
for (i in 1..5) {
println("$name index: $i")
}
}
}
fun main() {
println("currentThread name: " + Thread.currentThread().name)
val thread = MyThread()
thread.start()
// currentThread name: main
// Thread-0 index: 1
// Thread-0 index: 2
// Thread-0 index: 3
// Thread-0 index: 4
// Thread-0 index: 5
}
运行流程
以上两种不同创建线程的方式,其运行流程本质上都是一样的。