第10章 多线程
线程的创建与生命周期、同步与锁、线程通信——让程序同时做多件事。
本章学习目标
- 能通过继承 Thread 或实现 Runnable 创建线程
- 理解线程的生命周期与常用方法
- 能用 synchronized 解决线程安全问题
- 会用 wait/notify 做线程通信
本章导览
核心内容
线程概述
定义:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。例如同时运行的QQ、360安全卫士、Eclipse开发工具等。
说明:
在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天,但实际上这些进程并不是在同一时刻运行的。
在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程,操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。
由于CPU运行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。
定义:在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。
注意:操作系统中的每一个进程中都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。
单线程都是按照调用顺序依次往下执行,没有出现多段程序代码交替运行的效果,而多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行。
多线程可以充分利用CUP资源,进一步提升程序执行效率。
多线程看似是同时并发执行的,其实不然,它们和进程一样,也是由CPU控制并轮流执行的,只不过CPU运行速度非常快,故而给人同时执行的感觉。


📄 展开课件原文(第 11–14 页,共 4 页)
线程的创建
创建:Java为多线程开发提供了非常优秀的技术支持,在Java中,可以通过三种方式来实现多线程。
init(FilterConfig filterConfig)
1
继承Thread类,重写run()方法
2
实现Runnable接口,重写run()方法
3
实现Callable接口,重写call()方法,并使用Futrue来获取call()方法的返回结果
说明:Thread类是java.lang包下的一个线程类,用来实现Java多线程。
步骤:
创建一个Thread线程类的子类(子线程),同时重写Thread类的run()方法;
创建该子类的实例对象,并通过调用start()方法启动线程。
说明:Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程。在这种情况下,就可以考虑通过实现Runnable接口的方式来实现多线程。
创建一个Runnable接口的实现类,同时重写接口中的run()方法;
创建Runnable接口的实现类对象;
使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类的实例对象作为参数传入;
调用线程实例的start()方法启动线程。
说明:通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回结果。为了解决这个问题,从JDK 5开始,Java提供了一个新的Callable接口,来满足这种既能创建多线程又可以有返回值的需求。
使用:Callable接口实现多线程是通过Thread类的有参构造方法传入Runnable接口类型的参数来实现多线程,不同的是,这里传入的是Runnable接口的子类FutureTask对象作为参数,而FutureTask对象中则封装带有返回值的Callable接口实现类。
创建一个Callable接口的实现类,同时重写Callable接口的call()方法;
创建Callable接口的实现类对象;
通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象;
使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例;
说明:
Callable接口方式实现的多线程是通过FutureTask类来封装和管理返回结果的,该类的直接父接口是RunnableFuture。
FutureTask本质是Runnable接口和Future接口的实现类,而Future则是JDK 5提供的用来管理线程执行返回结果的。
| 方法声明 | 功能描述 |
|---|---|
| boolean cancel(boolean mayInterruptIfRunning) | 用于取消任务,参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行的任务 |
| boolean isCancelled() | 判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true |
| boolean isDone() | 判断任务是否已经完成,若任务完成,则返回true |
| V get() | 用于获取执行结果,这个方法会发生阻塞,一直等到任务执行完毕才返回执行结果 |
| V get(long timeout, TimeUnit unit) | 用于在指定时间内获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null |
通过实现Runnable接口(或者Callable接口)相对于继承Thread类实现多线程来说,有如下显著的好处:
适合多个线程去处理同一个共享资源的情况。
可以避免Java单继承带来的局限性。
说明:事实上,实际开发中大部分的多线程应用都会采用Runnable接口或者Callable接口的方式实现多线程。
。
对Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程运行,这个进程就会结束。
这里提到的前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程。
使用:如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。

📄 展开课件原文(第 15–23 页,共 9 页)
线程的生命周期及状态转换
说明:
在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。
当Thread对象创建完成时,线程的生命周期便开始了。
当线程任务中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。
线程状态转换图:
说明:创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。
NEW(新建状态)
说明:新建状态的线程调用start()方法,就会进入可运行状态。在RUNNABLE状态内部又可细分成两种状态:READY(就绪状态)和RUNNING(运行状态),并且线程可以在这两个状态之间相互转换。
RUNNABLE内部状态转换:
就绪状态:线程对象调用start()方法之后,等待JVM的调度,此时线程并没有运行;
运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。
RUNNABLE(可运行状态)
说明:运行状态的线程因为某些原因失去CPU的执行权,会进入阻塞状态。阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态。
线程进入阻塞状态的两种情况:
当线程A运行过程中,试图获取同步锁时,却被线程B获取;
当线程运行过程中,发出IO请求时。
BLOCKED(阻塞状态)
运行状态的线程调用了无时间参数限制的方法后,如wait()、join()等方法,就会转换为等待状态。
等待状态中的线程不能立即争夺CPU使用权,必须等待其他线程执行特定的操作后,才有机会争夺CPU使用权。
例如,调用wait()方法而处于等待状态中的线程,必须等待其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程;
调用join()方法而处于等待状态中的线程,必须等待其他加入的线程终止。
WAITING(等待状态)
运行状态中的线程调用了有时间参数限制的方法,如sleep(long millis)、wait(long timeout)、join(long millis)等方法,就会转换为定时等待状态。
定时等待状态中的线程不能立即争夺CPU使用权,必须等待其他相关线程执行完特定的操作或者限时时间结束后,才有机会再次争夺CPU使用权。
例如,调用了wait(long timeout) 方法而处于等待状态中的线程,需要通过其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。
TIMED_WAITING(定时等待状态)
线程的run()方法、call()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入终止状态。
一旦进入终止状态,线程将不再拥有运行的资格,也不能再转换到其他状态,生命周期结束。
TERMINATED(终止状态)

📄 展开课件原文(第 24–31 页,共 8 页)
线程的调度
定义:程序中的多个线程是并发执行的,但并不是同一时刻执行,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。
是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU时间片。
分时调度模型
是指让可运行池中所有就绪状态的线程争抢CPU的使用权,而优先级高的线程获取CPU执行权的概率大于优先级低的线程。
抢占式调度模型
(JVM默认方式)
VS
说明:
在应用程序中,要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。
线程的优先级用1~10之间的整数来表示,数字越大优先级越高。
除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级。
| Thread类的静态常量 | 功能描述 |
|---|---|
| static int MAX_PRIORITY | 表示线程的最高优先级,相当于值10 |
| static int MIN_PRIORITY | 表示线程的最低优先级,相当于值1 |
| static int NORM_PRIORIY | 表示线程的普通优先级,相当于值5 |
说明:程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。
使用:可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。
如果想要人为地控制线程执行顺序,使正在执行的线程暂停,将CPU使用权让给其他线程,这时可以使用静态方法sleep(long millis)。
该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态,这样其他的线程就可以得到执行的机会。
sleep(long millis)方法会声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。
线程让步可以通过yield()方法来实现,该方法和sleep(long millis)方法有点类似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。
当某个线程调用yield()方法之后,与当前线程优先级相同或者更高的线程可以获得执行的机会。
在Thread类中也提供了一个join()方法来实现线程插队功能。
当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。
Thread类中还提供了带有时间参数的线程插队方法join(long millis)。当执行带有时间参数的join(long millis)进行线程插队时,必须等待插入的线程指定时间过后才会继续执行其他线程。
📄 展开课件原文(第 32–37 页,共 6 页)
多线程同步
为什么要实现多线程同步?
多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。
说明:线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决线程安全问题,必须得保证处理共享资源的代码在任意时刻只能有一个线程访问。为此,Java中提供了线程同步机制。
同步代码块
同步方法
同步锁
实现线程
同步方式
语法:
注意:lock锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁。
synchronized(lock){
// 操作共享资源代码块...
}
lock是一个任意类型的锁对象
Synchronized关键字后大括号{}内包含的就是需要同步操作的共享资源代码块
原理:
当线程执行同步代码块时,首先会检查lock锁对象的标志位;
默认情况下标志位为1,此时线程会执行Synchronized同步代码块,同时将锁对象的标志位置为0;
当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后;
锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码,这样循环往复,直到共享资源被处理完为止。
说明:
在方法前面也可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。
[修饰符] synchronized 返回值类型 方法名([参数1,……]){}注意:
同步方法也有锁,它的锁就是当前调用该方法的对象,就是this指向的对象。
Java中静态方法的锁是该方法所在类的class对象,该对象可以直接类名.class的方式获取。
同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。
问题:synchronized同步代码块和同步方法使用一种封闭式的锁机制,使用起来非常简单,也能够解决线程同步过程中出现的线程安全问题,但也有一些限制,例如它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。
解决:从JDK 5开始,Java增加了一个功能更强大的Lock锁。Lock锁与synchronized隐式锁在功能上基本相同,其最大的优势在于Lock锁可以让某个线程在持续获取同步锁失败后返回,不再继续等待,另外Lock锁在使用时也更加灵活。
ReentrantLock类是Lock锁接口的实现类,也是常用的同步锁,在该同步锁中除了lock()方法和unlock()方法外,还提供了一些其他同步锁操作的方法,例如tryLock()方法可以判断某个线程锁是否可用。
在使用Lock同步锁时,可以根据需要在不同代码位置灵活的上锁和解锁,为了保证所有情况下都能正常解锁以确保其他线程可以执行,通常情况下会在finally{}代码块中调用unlock()方法来解锁。
定义:两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。
避免死锁方式:
加锁顺序(线程按照一定的顺序加锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
死锁检测
📄 展开课件原文(第 38–46 页,共 9 页)
多线程通信
为什么需要多线程通信?
现代社会崇尚合作精神,分工合作在日常生活和工作中无处不在。举个简单的例子,例如一条生产线的上下两个工序,它们必须以规定的速率完成各自的工作,才能保证产品在流水线中顺利的流转。如果下工序过慢,会造成产品在两道工序之间的积压,如果上工序过慢,会造成下工序长时间无事可做。在多线程的程序中,上下工序可以看作两个线程,这两个线程之间需要协同完成工作,就需要线程之间进行通信。
思路:如果想解决上述线程执行不一致的问题,就需要控制多个线程按照一定的顺序轮流执行,此时就需要让线程间进行通信,保证线程任务的协调进行。
解决:为此,Java在Object类中提供了wait()、notify()、notifyAll()等方法用于解决线程间的通信问题,因为Java中所有类都是Object类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。
说明:其中wait()方法用于使当前线程进入等待状态,notify()和notifyAll()方法用于唤醒当前处于等待状态的线程。
注意:wait()、notify()和notifyAll()方法的调用者都应该是同步锁对象,如果这三个方法的调用者不是同步锁对象,Java虚拟机就会抛出IllegalMonitorStateException异常。
| 方法声明 | 功能描述 |
|---|---|
| void wait() | 使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用notify()或notifyAll()方法唤醒该线程为止 |
| void notify() | 唤醒此同步锁上等待的第一个调用wait()方法的线程 |
| void notifyAll() | 唤醒此同步锁上调用wait()方法的所有线程 |
📄 展开课件原文(第 47–49 页,共 3 页)
线程池
为什么需要线程池技术?
在前面几个小节中讲解了基础的多线程实现,对于基本的任务来说是足够的,但是对于更复杂的任务来说这种频繁手动式的创建、管理线程显然是不可取的,因为线程对象使用了大量的内存,在大规模应用程序中,创建、分配和释放多线程对象会产生大量内存管理开销。为此,可以考虑使用Java提供的线程池来创建多线程,进一步优化线程管理。
步骤:
创建一个实现Runnable接口或者Callable接口的实现类,同时重写run()或者call()方法;
创建Runnable接口或者Callable接口的实现类对象;
使用Executors线程执行器类创建线程池;
使用ExecutorService执行器服务类的submit()方法将Runnable接口或者Callable接口的实现类对象提交到线程池进行管理;
线程任务执行完成后,可以使用shutdown()方法关闭线程池。
10.7.1 Executor接口实现线程池管理——Executors创建线程池的方法
| 方法声明 | 功能描述 |
|---|---|
| ExecutorService newCachedThreadPool() | 创建一个可扩展线程池的执行器。这个线程池执行器适用于启动许多短期任务的应用程序 |
| ExecutorService newFixedThreadPool( int nThreads) | 创建一个固定线程数量线程池的执行器。这种线程池执行器可以很好的控制多线程任务,也不会导致由于响应过多导致的程序崩溃 |
| ExecutorService newSingleThreadExecutor() | 在特殊需求下创建一个只执行一个任务的单个线程 |
| ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个定长线程池,支持定时及周期性任务执行 |
说明:在使用Callable接口实现多线程时,会用到FutureTask类对线程执行结果进行管理和获取,由于该类在获取结果时是通过阻塞或者轮询的方式,违背多线程编程的初衷且耗费过多资源。
解决:JDK 8中新增了一个强大的函数式异步编程辅助类CompletableFuture,该类同时实现了Future接口和CompletionStage接口(Java 8中新增的一个线程任务完成结果接口),并对Future进行了强大的扩展,简化异步编程的复杂性。
CompletableFuture对象创建的四个静态方法:
| 方法声明 | 功能描述 |
|---|---|
| static CompletableFuture<Void> runAsync(Runnable runnable) | 以Runnable函数式接口类型为参数,并使用ForkJoinPool.commonPool()作为它的线程池执行异步代码获取CompletableFuture计算结果为空的对象 |
| static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor) | 以Runnable函数式接口类型为参数,并传入指定的线程池执行器executor来获取CompletableFuture计算结果为空的对象 |
| static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) | 以Supplier函数式接口类型为参数,并使用ForkJoinPool.commonPool()作为它的线程池执行异步代码获取CompletableFuture计算结果非空的对象 |
| static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) | 以Supplier函数式接口类型为参数,并传入指定的线程池执行器executor来获取CompletableFuture计算结果非空的对象 |
📄 展开课件原文(第 50–54 页,共 5 页)
本章小结
章主要讲解了线程的创建、线程的生命周期和状态转换、线程的调度、线程同步、线程通信以及线程池等方面的知识。这里讲解的几乎每一个知识点都是多线程的重点内容,需要认真学习和体会。
📄 展开课件原文(第 55–56 页,共 2 页)
课件里的提醒 课件原文中的“注意 / 说明 / 定义”
- 定义:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。例如同时运行的QQ、360安全卫士、Eclipse开发工具等。
- 定义:在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。
- 注意:操作系统中的每一个进程中都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。
- 说明:Thread类是java.lang包下的一个线程类,用来实现Java多线程。
- 说明:Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程。在这种情况下,就可以考虑通过实现Runnable接口的方式来实现多线程。
- 说明:通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回结果。为了解决这个问题,从JDK 5开始,Java提供了一个新的Callable接口,来满足这种既能创建多线程又可以有返回值的需求。
- 说明:事实上,实际开发中大部分的多线程应用都会采用Runnable接口或者Callable接口的方式实现多线程。
- 说明:创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。
- 说明:新建状态的线程调用start()方法,就会进入可运行状态。在RUNNABLE状态内部又可细分成两种状态:READY(就绪状态)和RUNNING(运行状态),并且线程可以在这两个状态之间相互转换。
- 说明:运行状态的线程因为某些原因失去CPU的执行权,会进入阻塞状态。阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态。
- 例如,调用wait()方法而处于等待状态中的线程,必须等待其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程;
- 例如,调用了wait(long timeout) 方法而处于等待状态中的线程,需要通过其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。
本章代码速览 把课件里的示例代码集中起来,可直接复制运行
synchronized(lock){// 操作共享资源代码块[修饰符] synchronized 返回值类型 方法名([参数1,……]){}