线程概述
所有运行中的任务通常对应一个 进程(Process)。当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
进程包含如下三个特征:
- 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
- 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态。
- 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。
并发性(concurrency)和 并行性(parallel)是两个概念。
并行 指在同一时刻,有多条指令在多个处理器上同时执行;
并发 指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,造成在宏观上具有多个进程同时执行的假象。
多线程扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread) 也被称作 轻量级进程(Lightweight Process),线程是进程的执行单元。线程在进程中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于大多数应用程序通常仅要求有一个主线程,但也可以在该进程内创建多个线程,每个线程也是相互独立。
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不用有系统资源,它与父进程的其他线程共享该进程所有的全部资源。
线程是独立运行的,它并不知道进程中是否还有其他线程的存在。线程的执行时 抢占式 的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
线程的创建和启动
通过继承 Thread 类来创建线程类
- 定义 Thread 类的子类并重写该类的 run() 方法;
- 创建 Thread 子类的实例,即创建了线程对象;
- 调用线程对象的 start() 方法来启动线程。
使用继承 Thread 类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。
Thread.currentThread():静态方法,该方法总是返回当前正在执行的线程对象。
getName():该方法是 Thread 类的实例方法,该方法返回调用该方法的线程名字。也可以通过 setName(String name) 方法为线程设置名字。
实现 Runnable 接口创建线程类
- 定义 Runnable 接口的实现类,并重写该接口的 run() 方法,该 fun() 方法的方法体同样是该线程的线程执行体。
- 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 才是真正的线程对象。
- 调用线程对象的 start() 方法启动线程。
使用 Callable 和 Future 创建线程
前面通过实现 Runnalbe 接口创建多线程时,Thread 类的作用就是把 run() 方法包装成线程执行体。从Java5开始,提供了可以直接把任意方法都包装成线程执行体的方式。
Callable 接口是 Runnable 接口的增强版,它提供了一个 call() 方法可以作为线程执行体:
- call() 方法可以有返回值。
- call() 方法可以声明抛出异常。
因此完全可以提供一个 Callable 对象作为 Thread 的 target ,而该线程的线程执行体就是该 Callable 对象的 call() 方法。由于 Callable 接口并不是 Runnable 接口的子接口,所以 Callable 对象不能直接作为 Thread 的 target。而且 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() 方法的返回值。该方法让程序最多阻塞 timeout 和 unit 指定的时间,如果经过指定时间后 Callable 任务依然没有返回值,将会抛出 TimeoutException 异常。
- boolean isCancelled():如果在 Callable 任务正常完成前被取消,则返回 true。
- boolean isDone():如果 Callable 任务已经完成,则返回 true。
Callable 接口有泛型限制,接口里的泛型形参类型与 call() 方法返回值类型相同
通过 Callable 和 Future 创建并启动有返回值的线程:
- 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,且该 call() 方法有返回值,再创建 Callable 实现类的实例。可以直接使用 Lambda表达式创建 Callable 对象。
- 使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 的返回值。
- 使用 FutrueTask 对象作为 Thread 对象的 target 创建并启动新线程。
- 调用 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() 方法执行完成,线程正常结束。
- 线程抛出一个未捕获的 Exception 或 Error 终止了 run 方法而意外死亡。
为了测试某个线程是否已经死亡,可以调用线程对象的 isAlive() 方法,当线程处于就绪、运行、阻塞三种状态时,该方法将返回 true;当线程处于新建、死亡两种状态时,该方法返回 false。不要试图对一个已经死亡的线程调用 start() 方法使它重新启动,死亡就是死亡,该线程将不可以再次作为线程执行。