16-多线程基础

线程概述

所有运行中的任务通常对应一个 进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。

进程包含如下三个特征:

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。

并发性(concurrency)并行性(parallel)是两个概念。
并行 指在同一时刻,有多条指令在多个处理器上同时执行;
并发 指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,造成在宏观上具有多个进程同时执行的假象。

多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread) 也被称作 轻量级进程(Lightweight Process),线程是进程的执行单元。线程在进程中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于大多数应用程序通常仅要求有一个主线程,但也可以在该进程内创建多个线程,每个线程也是相互独立。

线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不用有系统资源,它与父进程的其他线程共享该进程所有的全部资源。

线程是独立运行的,它并不知道进程中是否还有其他线程的存在。线程的执行时 抢占式 的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

线程的创建和启动

通过继承 Thread 类来创建线程类

  1. 定义 Thread 类的子类并重写该类的 run() 方法;
  2. 创建 Thread 子类的实例,即创建了线程对象;
  3. 调用线程对象的 start() 方法来启动线程。

使用继承 Thread 类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

Thread.currentThread():静态方法,该方法总是返回当前正在执行的线程对象。
getName():该方法是 Thread 类的实例方法,该方法返回调用该方法的线程名字。也可以通过 setName(String name) 方法为线程设置名字。

实现 Runnable 接口创建线程类

  1. 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 fun() 方法的方法体同样是该线程的线程执行体。
  2. 创建 Runnable 实现类的实例,并以此实例作为 Threadtarget 来创建 Thread 对象,该 Thread 才是真正的线程对象。
  3. 调用线程对象的 start() 方法启动线程。

使用 Callable 和 Future 创建线程

前面通过实现 Runnalbe 接口创建多线程时,Thread 类的作用就是把 run() 方法包装成线程执行体。从Java5开始,提供了可以直接把任意方法都包装成线程执行体的方式。

Callable 接口是 Runnable 接口的增强版,它提供了一个 call() 方法可以作为线程执行体:

  • call() 方法可以有返回值。
  • call() 方法可以声明抛出异常。

因此完全可以提供一个 Callable 对象作为 Threadtarget ,而该线程的线程执行体就是该 Callable 对象的 call() 方法。由于 Callable 接口并不是 Runnable 接口的子接口,所以 Callable 对象不能直接作为 Threadtarget。而且 call() 方法还有一个返回值,call() 方法并不是直接调用,他是作为线程执行体被调用的。

Future 接口是用来代表 Callable 接口里 call() 方法的返回值,并为 Future 接口提供了一个 FutureTask 实现类,该实现类实现了 Future 接口,并实现了 Runnable 接口,因此可以作为 Thread 类的 target

Future 接口里定义了如下几个公共方法来控制它关联的 Callable 任务:

  • boolean cancel(boolean mayInterrupIfRunning):试图取消该 Future 里关联的 Callable 任务。
  • V get():返回 Callable 任务里 call() 方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值。
  • V get(long timeout,TimeUnit unit):返回 Callable 任务里 call() 方法的返回值。该方法让程序最多阻塞 timeoutunit 指定的时间,如果经过指定时间后 Callable 任务依然没有返回值,将会抛出 TimeoutException 异常。
  • boolean isCancelled():如果在 Callable 任务正常完成前被取消,则返回 true
  • boolean isDone():如果 Callable 任务已经完成,则返回 true

Callable 接口有泛型限制,接口里的泛型形参类型与 call() 方法返回值类型相同

通过 CallableFuture 创建并启动有返回值的线程:

  1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,且该 call() 方法有返回值,再创建 Callable 实现类的实例。可以直接使用 Lambda表达式创建 Callable 对象。
  2. 使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 的返回值。
  3. 使用 FutrueTask 对象作为 Thread 对象的 target 创建并启动新线程。
  4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

线程的生命周期

当线程被创建以后,它既不是一启动就进入执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过 NEW(新创建)、RUNNABLE(可运行)、BLOCKED(被阻塞)、WAITING(等待)、TIMED_WAITING(计时等待)、TERMINATED(被终止) 6种状态。尤其是当线程启动以后,它不可能一直霸占着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也就会多次在运行、阻塞之间切换。

新建和就绪状态

使用 new 创建了一个线程后,该线程就处于新建状态,JVM为其分配内存并初始化其成员变量的值。当线程对象调用了 start() 方法后,该线程处于就绪状态,JVM会为其创建线程栈和程序计数器,但此时该线程还没有真正开始运行,只是表示该线程可以运行。至于何时开始运行该线程,取决于JVM里线程调度器的调度。

如果希望调用子线程的 start() 方法后子线程立即开始执行,程序可以使用 Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒,1毫秒就足够了,因为在这1毫秒内CPU不会空闲,它会去执行另一个处于就绪状态的线程,这样就可以让子线程立即开始执行。

可运行状态

一旦调用了 start()方法,线程处于 Runnable 状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。

当一个线程开始运行后,它不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。对于采用抢占式策略的系统而言,系统会给每个可执行的线程一个时间片段来处理任务,当该时间片段用完后,系统就会剥夺该线程所占用的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会优先考虑线程的优先级。

如今桌面系统都采用抢占式调度策略,但一些小型设备如手机可能会采用协作式调度策略,在这样的调度方式中,只有当一个线程调用了它的 sleep()yeld() 方法后才会放弃所占用的资源-也就是必须由该线程主动放弃所占用的资源。

被阻塞线程和等待线程

当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。当发生以下情况时,线程将会进入阻塞或等待状态:

  • 线程试图获得一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
  • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用 Object.wait() 方法或 Thread.join() 方法,或者是等待 java.util.concurrent 中的 Lock 或 Condition 时,就会出现这种情况。注意,被阻塞状态与等待状态是有很大不同的。
  • 有几个方法有一个超时参数。调用它们导致线程进入 计时等待(timed waiting) 状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有 Thread.sleep()、Object.wait()、Thread.join()、Lock.tryLock()、Condition.await() 的计时版本。

线程死亡

线程会以如下三种方式结束,结束后就处于死亡状态:

  • run()call() 方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的 ExceptionError 终止了 run 方法而意外死亡。

为了测试某个线程是否已经死亡,可以调用线程对象的 isAlive() 方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回 true;当线程处于新建、死亡两种状态时,该方法返回 false不要试图对一个已经死亡的线程调用 start() 方法使它重新启动,死亡就是死亡,该线程将不可以再次作为线程执行。