多线程

第10章 多线程

线程的创建与生命周期、同步与锁、线程通信——让程序同时做多件事。

并发56 页课件8 个知识点

本章学习目标

  • 能通过继承 Thread 或实现 Runnable 创建线程
  • 理解线程的生命周期与常用方法
  • 能用 synchronized 解决线程安全问题
  • 会用 wait/notify 做线程通信

本章导览

核心内容

10.1

线程概述

定义:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。例如同时运行的QQ、360安全卫士、Eclipse开发工具等。

说明:

在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天,但实际上这些进程并不是在同一时刻运行的。

在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程,操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。

由于CPU运行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。

定义:在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。

注意:操作系统中的每一个进程中都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。

单线程都是按照调用顺序依次往下执行,没有出现多段程序代码交替运行的效果,而多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行。

多线程可以充分利用CUP资源,进一步提升程序执行效率。

多线程看似是同时并发执行的,其实不然,它们和进程一样,也是由CPU控制并轮流执行的,只不过CPU运行速度非常快,故而给人同时执行的感觉。

第11页 · 课件截图
第11页 · 课件截图
第14页 · 课件截图
第14页 · 课件截图
📄 展开课件原文(第 11–14 页,共 4 页)
第 11 页

10.1 线程概述

定义:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。例如同时运行的QQ、360安全卫士、Eclipse开发工具等。

10.1.1 进程

第 12 页

10.1 线程概述

说明:

在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天,但实际上这些进程并不是在同一时刻运行的。

在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程,操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。

由于CPU运行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。

第 13 页

10.1 线程概述

定义:在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。

注意:操作系统中的每一个进程中都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。

10.1.2 线程

第 14 页

10.1 线程概述

说明:

单线程都是按照调用顺序依次往下执行,没有出现多段程序代码交替运行的效果,而多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行。

多线程可以充分利用CUP资源,进一步提升程序执行效率。

多线程看似是同时并发执行的,其实不然,它们和进程一样,也是由CPU控制并轮流执行的,只不过CPU运行速度非常快,故而给人同时执行的感觉。

10.1.2 线程——单线程与多线程

10.2

线程的创建

创建: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提供的用来管理线程执行返回结果的。

课件表格 · 第21页
方法声明功能描述
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)语句,这个线程就变成一个后台线程。

第20页 · 课件截图
第20页 · 课件截图
📄 展开课件原文(第 15–23 页,共 9 页)
第 15 页

10.2 线程的创建

创建:Java为多线程开发提供了非常优秀的技术支持,在Java中,可以通过三种方式来实现多线程。

init(FilterConfig filterConfig)

1

继承Thread类,重写run()方法

2

实现Runnable接口,重写run()方法

3

实现Callable接口,重写call()方法,并使用Futrue来获取call()方法的返回结果

第 16 页

10.2 线程的创建

说明:Thread类是java.lang包下的一个线程类,用来实现Java多线程。

步骤:

创建一个Thread线程类的子类(子线程),同时重写Thread类的run()方法;

创建该子类的实例对象,并通过调用start()方法启动线程。

10.2.1 Thread类实现多线程

第 17 页

10.2 线程的创建

说明:Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程。在这种情况下,就可以考虑通过实现Runnable接口的方式来实现多线程。

步骤:

创建一个Runnable接口的实现类,同时重写接口中的run()方法;

创建Runnable接口的实现类对象;

使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类的实例对象作为参数传入;

调用线程实例的start()方法启动线程。

10.2.2 Runnable接口实现多线程

第 18 页

10.2 线程的创建

说明:通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回结果。为了解决这个问题,从JDK 5开始,Java提供了一个新的Callable接口,来满足这种既能创建多线程又可以有返回值的需求。

使用:Callable接口实现多线程是通过Thread类的有参构造方法传入Runnable接口类型的参数来实现多线程,不同的是,这里传入的是Runnable接口的子类FutureTask对象作为参数,而FutureTask对象中则封装带有返回值的Callable接口实现类。

10.2.3 Callable接口实现多线程

第 19 页

10.2 线程的创建

步骤:

创建一个Callable接口的实现类,同时重写Callable接口的call()方法;

创建Callable接口的实现类对象;

通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象;

使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例;

调用线程实例的start()方法启动线程。

10.2.3 Callable接口实现多线程

第 20 页

10.2 线程的创建

说明:

Callable接口方式实现的多线程是通过FutureTask类来封装和管理返回结果的,该类的直接父接口是RunnableFuture。

FutureTask本质是Runnable接口和Future接口的实现类,而Future则是JDK 5提供的用来管理线程执行返回结果的。

10.2.3 Callable接口实现多线程——FutureTask继承关系图

第 21 页

10.2 线程的创建

10.2.3 Callable接口实现多线程——Future接口方法

方法声明功能描述
boolean cancel(boolean mayInterruptIfRunning)用于取消任务,参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行的任务
boolean isCancelled()判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true
boolean isDone()判断任务是否已经完成,若任务完成,则返回true
V get()用于获取执行结果,这个方法会发生阻塞,一直等到任务执行完毕才返回执行结果
V get(long timeout, TimeUnit unit)用于在指定时间内获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null
第 22 页

10.2 线程的创建

通过实现Runnable接口(或者Callable接口)相对于继承Thread类实现多线程来说,有如下显著的好处:

适合多个线程去处理同一个共享资源的情况。

可以避免Java单继承带来的局限性。

说明:事实上,实际开发中大部分的多线程应用都会采用Runnable接口或者Callable接口的方式实现多线程。

。

10.2.4 三种实现多线程方式的对比分析

第 23 页

10.2 线程的创建

说明:

对Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程运行,这个进程就会结束。

这里提到的前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程。

使用:如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。

10.2.5 后台线程

10.3

线程的生命周期及状态转换

说明:

在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(终止状态)

第25页 · 课件截图
第25页 · 课件截图
📄 展开课件原文(第 24–31 页,共 8 页)
第 24 页

10.3 线程的生命周期及状态转换

说明:

在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。

当Thread对象创建完成时,线程的生命周期便开始了。

当线程任务中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。

第 25 页

10.3 线程的生命周期及状态转换

线程状态转换图:

第 26 页

10.3 线程的生命周期及状态转换

说明:创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。

NEW(新建状态)

第 27 页

10.3 线程的生命周期及状态转换

说明:新建状态的线程调用start()方法,就会进入可运行状态。在RUNNABLE状态内部又可细分成两种状态:READY(就绪状态)和RUNNING(运行状态),并且线程可以在这两个状态之间相互转换。

RUNNABLE内部状态转换:

就绪状态:线程对象调用start()方法之后,等待JVM的调度,此时线程并没有运行;

运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。

RUNNABLE(可运行状态)

第 28 页

10.3 线程的生命周期及状态转换

说明:运行状态的线程因为某些原因失去CPU的执行权,会进入阻塞状态。阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态。

线程进入阻塞状态的两种情况:

当线程A运行过程中,试图获取同步锁时,却被线程B获取;

当线程运行过程中,发出IO请求时。

BLOCKED(阻塞状态)

第 29 页

10.3 线程的生命周期及状态转换

说明:

运行状态的线程调用了无时间参数限制的方法后,如wait()、join()等方法,就会转换为等待状态。

等待状态中的线程不能立即争夺CPU使用权,必须等待其他线程执行特定的操作后,才有机会争夺CPU使用权。

例如,调用wait()方法而处于等待状态中的线程,必须等待其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程;

调用join()方法而处于等待状态中的线程,必须等待其他加入的线程终止。

WAITING(等待状态)

第 30 页

10.3 线程的生命周期及状态转换

说明:

运行状态中的线程调用了有时间参数限制的方法,如sleep(long millis)、wait(long timeout)、join(long millis)等方法,就会转换为定时等待状态。

定时等待状态中的线程不能立即争夺CPU使用权,必须等待其他相关线程执行完特定的操作或者限时时间结束后,才有机会再次争夺CPU使用权。

例如,调用了wait(long timeout) 方法而处于等待状态中的线程,需要通过其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。

TIMED_WAITING(定时等待状态)

第 31 页

10.3 线程的生命周期及状态转换

说明:

线程的run()方法、call()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入终止状态。

一旦进入终止状态,线程将不再拥有运行的资格,也不能再转换到其他状态,生命周期结束。

TERMINATED(终止状态)

10.4

线程的调度

定义:程序中的多个线程是并发执行的,但并不是同一时刻执行,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。

是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU时间片。

分时调度模型

是指让可运行池中所有就绪状态的线程争抢CPU的使用权,而优先级高的线程获取CPU执行权的概率大于优先级低的线程。

抢占式调度模型

(JVM默认方式)

VS

说明:

在应用程序中,要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。

线程的优先级用1~10之间的整数来表示,数字越大优先级越高。

除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级。

课件表格 · 第34页
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 页)
第 32 页

10.4 线程的调度

定义:程序中的多个线程是并发执行的,但并不是同一时刻执行,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。

是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU时间片。

分时调度模型

是指让可运行池中所有就绪状态的线程争抢CPU的使用权,而优先级高的线程获取CPU执行权的概率大于优先级低的线程。

抢占式调度模型

(JVM默认方式)

VS

第 33 页

10.4 线程的调度

说明:

在应用程序中,要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。

线程的优先级用1~10之间的整数来表示,数字越大优先级越高。

除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级。

10.4.1 线程的优先级

第 34 页

10.4 线程的调度

10.4.1 线程的优先级——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类的三个静态常量。

第 35 页

10.4 线程的调度

说明:

如果想要人为地控制线程执行顺序,使正在执行的线程暂停,将CPU使用权让给其他线程,这时可以使用静态方法sleep(long millis)。

该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态,这样其他的线程就可以得到执行的机会。

sleep(long millis)方法会声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。

10.4.2 线程休眠

第 36 页

10.4 线程的调度

说明:

线程让步可以通过yield()方法来实现,该方法和sleep(long millis)方法有点类似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。

当某个线程调用yield()方法之后,与当前线程优先级相同或者更高的线程可以获得执行的机会。

10.4.3 线程让步

第 37 页

10.4 线程的调度

说明:

在Thread类中也提供了一个join()方法来实现线程插队功能。

当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。

Thread类中还提供了带有时间参数的线程插队方法join(long millis)。当执行带有时间参数的join(long millis)进行线程插队时,必须等待插入的线程指定时间过后才会继续执行其他线程。

10.4.4 线程插队

10.5

多线程同步

为什么要实现多线程同步?

多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。

说明:线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决线程安全问题,必须得保证处理共享资源的代码在任意时刻只能有一个线程访问。为此,Java中提供了线程同步机制。

同步代码块

同步方法

同步锁

实现线程

同步方式

语法:

注意:lock锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁。

JAVA
synchronized(lock){
// 操作共享资源代码块

...

}

lock是一个任意类型的锁对象

Synchronized关键字后大括号{}内包含的就是需要同步操作的共享资源代码块

原理:

当线程执行同步代码块时,首先会检查lock锁对象的标志位;

默认情况下标志位为1,此时线程会执行Synchronized同步代码块,同时将锁对象的标志位置为0;

当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后;

锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码,这样循环往复,直到共享资源被处理完为止。

说明:

在方法前面也可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。

被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。

JAVA
[修饰符] 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 页)
第 38 页

10.5 多线程同步

为什么要实现多线程同步?

多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。

第 39 页

10.5 多线程同步

说明:线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决线程安全问题,必须得保证处理共享资源的代码在任意时刻只能有一个线程访问。为此,Java中提供了线程同步机制。

10.5.1 线程安全

同步代码块

同步方法

同步锁

实现线程

同步方式

第 40 页

10.5 多线程同步

语法:

注意:lock锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的。锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁。

10.5.2 同步代码块

synchronized(lock){

// 操作共享资源代码块

...

}

lock是一个任意类型的锁对象

Synchronized关键字后大括号{}内包含的就是需要同步操作的共享资源代码块

第 41 页

10.5 多线程同步

原理:

当线程执行同步代码块时,首先会检查lock锁对象的标志位;

默认情况下标志位为1,此时线程会执行Synchronized同步代码块,同时将锁对象的标志位置为0;

当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后;

锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码,这样循环往复,直到共享资源被处理完为止。

10.5.2 同步代码块

第 42 页

10.5 多线程同步

语法:

说明:

在方法前面也可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。

被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。

10.5.3 同步方法

[修饰符] synchronized 返回值类型 方法名([参数1,……]){}

第 43 页

10.5 多线程同步

注意:

同步方法也有锁,它的锁就是当前调用该方法的对象,就是this指向的对象。

Java中静态方法的锁是该方法所在类的class对象,该对象可以直接类名.class的方式获取。

同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。

10.5.3 同步方法

第 44 页

10.5 多线程同步

问题:synchronized同步代码块和同步方法使用一种封闭式的锁机制,使用起来非常简单,也能够解决线程同步过程中出现的线程安全问题,但也有一些限制,例如它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。

解决:从JDK 5开始,Java增加了一个功能更强大的Lock锁。Lock锁与synchronized隐式锁在功能上基本相同,其最大的优势在于Lock锁可以让某个线程在持续获取同步锁失败后返回,不再继续等待,另外Lock锁在使用时也更加灵活。

10.5.4 同步锁

第 45 页

10.5 多线程同步

注意:

ReentrantLock类是Lock锁接口的实现类,也是常用的同步锁,在该同步锁中除了lock()方法和unlock()方法外,还提供了一些其他同步锁操作的方法,例如tryLock()方法可以判断某个线程锁是否可用。

在使用Lock同步锁时,可以根据需要在不同代码位置灵活的上锁和解锁,为了保证所有情况下都能正常解锁以确保其他线程可以执行,通常情况下会在finally{}代码块中调用unlock()方法来解锁。

10.5.4 同步锁

第 46 页

10.5 多线程同步

定义:两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。

避免死锁方式:

加锁顺序(线程按照一定的顺序加锁)

加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)

死锁检测

10.5.5 死锁问题

10.6

多线程通信

为什么需要多线程通信?

现代社会崇尚合作精神,分工合作在日常生活和工作中无处不在。举个简单的例子,例如一条生产线的上下两个工序,它们必须以规定的速率完成各自的工作,才能保证产品在流水线中顺利的流转。如果下工序过慢,会造成产品在两道工序之间的积压,如果上工序过慢,会造成下工序长时间无事可做。在多线程的程序中,上下工序可以看作两个线程,这两个线程之间需要协同完成工作,就需要线程之间进行通信。

思路:如果想解决上述线程执行不一致的问题,就需要控制多个线程按照一定的顺序轮流执行,此时就需要让线程间进行通信,保证线程任务的协调进行。

解决:为此,Java在Object类中提供了wait()、notify()、notifyAll()等方法用于解决线程间的通信问题,因为Java中所有类都是Object类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。

说明:其中wait()方法用于使当前线程进入等待状态,notify()和notifyAll()方法用于唤醒当前处于等待状态的线程。

注意:wait()、notify()和notifyAll()方法的调用者都应该是同步锁对象,如果这三个方法的调用者不是同步锁对象,Java虚拟机就会抛出IllegalMonitorStateException异常。

课件表格 · 第49页
方法声明功能描述
void wait()使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用notify()或notifyAll()方法唤醒该线程为止
void notify()唤醒此同步锁上等待的第一个调用wait()方法的线程
void notifyAll()唤醒此同步锁上调用wait()方法的所有线程
📄 展开课件原文(第 47–49 页,共 3 页)
第 47 页

10.6 多线程通信

为什么需要多线程通信?

现代社会崇尚合作精神,分工合作在日常生活和工作中无处不在。举个简单的例子,例如一条生产线的上下两个工序,它们必须以规定的速率完成各自的工作,才能保证产品在流水线中顺利的流转。如果下工序过慢,会造成产品在两道工序之间的积压,如果上工序过慢,会造成下工序长时间无事可做。在多线程的程序中,上下工序可以看作两个线程,这两个线程之间需要协同完成工作,就需要线程之间进行通信。

第 48 页

10.6 多线程通信

思路:如果想解决上述线程执行不一致的问题,就需要控制多个线程按照一定的顺序轮流执行,此时就需要让线程间进行通信,保证线程任务的协调进行。

解决:为此,Java在Object类中提供了wait()、notify()、notifyAll()等方法用于解决线程间的通信问题,因为Java中所有类都是Object类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。

10.6.2 问题如何解决

第 49 页

10.6 多线程通信

说明:其中wait()方法用于使当前线程进入等待状态,notify()和notifyAll()方法用于唤醒当前处于等待状态的线程。

注意:wait()、notify()和notifyAll()方法的调用者都应该是同步锁对象,如果这三个方法的调用者不是同步锁对象,Java虚拟机就会抛出IllegalMonitorStateException异常。

10.6.2 问题如何解决——线程通信的常用方法

方法声明功能描述
void wait()使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用notify()或notifyAll()方法唤醒该线程为止
void notify()唤醒此同步锁上等待的第一个调用wait()方法的线程
void notifyAll()唤醒此同步锁上调用wait()方法的所有线程
10.7

线程池

为什么需要线程池技术?

在前面几个小节中讲解了基础的多线程实现,对于基本的任务来说是足够的,但是对于更复杂的任务来说这种频繁手动式的创建、管理线程显然是不可取的,因为线程对象使用了大量的内存,在大规模应用程序中,创建、分配和释放多线程对象会产生大量内存管理开销。为此,可以考虑使用Java提供的线程池来创建多线程,进一步优化线程管理。

步骤:

创建一个实现Runnable接口或者Callable接口的实现类,同时重写run()或者call()方法;

创建Runnable接口或者Callable接口的实现类对象;

使用Executors线程执行器类创建线程池;

使用ExecutorService执行器服务类的submit()方法将Runnable接口或者Callable接口的实现类对象提交到线程池进行管理;

线程任务执行完成后,可以使用shutdown()方法关闭线程池。

10.7.1 Executor接口实现线程池管理——Executors创建线程池的方法

课件表格 · 第52页
方法声明功能描述
ExecutorService newCachedThreadPool()创建一个可扩展线程池的执行器。这个线程池执行器适用于启动许多短期任务的应用程序
ExecutorService newFixedThreadPool( int nThreads)创建一个固定线程数量线程池的执行器。这种线程池执行器可以很好的控制多线程任务,也不会导致由于响应过多导致的程序崩溃
ExecutorService newSingleThreadExecutor()在特殊需求下创建一个只执行一个任务的单个线程
ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个定长线程池,支持定时及周期性任务执行

说明:在使用Callable接口实现多线程时,会用到FutureTask类对线程执行结果进行管理和获取,由于该类在获取结果时是通过阻塞或者轮询的方式,违背多线程编程的初衷且耗费过多资源。

解决:JDK 8中新增了一个强大的函数式异步编程辅助类CompletableFuture,该类同时实现了Future接口和CompletionStage接口(Java 8中新增的一个线程任务完成结果接口),并对Future进行了强大的扩展,简化异步编程的复杂性。

CompletableFuture对象创建的四个静态方法:

课件表格 · 第54页
方法声明功能描述
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 页)
第 50 页

10.7 线程池

为什么需要线程池技术?

在前面几个小节中讲解了基础的多线程实现,对于基本的任务来说是足够的,但是对于更复杂的任务来说这种频繁手动式的创建、管理线程显然是不可取的,因为线程对象使用了大量的内存,在大规模应用程序中,创建、分配和释放多线程对象会产生大量内存管理开销。为此,可以考虑使用Java提供的线程池来创建多线程,进一步优化线程管理。

第 51 页

10.7 线程池

步骤:

创建一个实现Runnable接口或者Callable接口的实现类,同时重写run()或者call()方法;

创建Runnable接口或者Callable接口的实现类对象;

使用Executors线程执行器类创建线程池;

使用ExecutorService执行器服务类的submit()方法将Runnable接口或者Callable接口的实现类对象提交到线程池进行管理;

线程任务执行完成后,可以使用shutdown()方法关闭线程池。

10.7.1 Executor接口实现线程池管理

第 52 页

10.7 线程池

10.7.1 Executor接口实现线程池管理——Executors创建线程池的方法

方法声明功能描述
ExecutorService newCachedThreadPool()创建一个可扩展线程池的执行器。这个线程池执行器适用于启动许多短期任务的应用程序
ExecutorService newFixedThreadPool( int nThreads)创建一个固定线程数量线程池的执行器。这种线程池执行器可以很好的控制多线程任务,也不会导致由于响应过多导致的程序崩溃
ExecutorService newSingleThreadExecutor()在特殊需求下创建一个只执行一个任务的单个线程
ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个定长线程池,支持定时及周期性任务执行
第 53 页

10.7 线程池

说明:在使用Callable接口实现多线程时,会用到FutureTask类对线程执行结果进行管理和获取,由于该类在获取结果时是通过阻塞或者轮询的方式,违背多线程编程的初衷且耗费过多资源。

解决:JDK 8中新增了一个强大的函数式异步编程辅助类CompletableFuture,该类同时实现了Future接口和CompletionStage接口(Java 8中新增的一个线程任务完成结果接口),并对Future进行了强大的扩展,简化异步编程的复杂性。

10.7.2 CompletableFuture类实现线程池管理

第 54 页

10.7 线程池

CompletableFuture对象创建的四个静态方法:

10.7.2 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计算结果非空的对象
10.8

本章小结

章主要讲解了线程的创建、线程的生命周期和状态转换、线程的调度、线程同步、线程通信以及线程池等方面的知识。这里讲解的几乎每一个知识点都是多线程的重点内容,需要认真学习和体会。

📄 展开课件原文(第 55–56 页,共 2 页)
第 55 页

章主要讲解了线程的创建、线程的生命周期和状态转换、线程的调度、线程同步、线程通信以及线程池等方面的知识。这里讲解的几乎每一个知识点都是多线程的重点内容,需要认真学习和体会。

10.8 本章小结

第 56 页

课件里的提醒 课件原文中的“注意 / 说明 / 定义”

  • 定义:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。例如同时运行的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()方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。

本章代码速览 把课件里的示例代码集中起来,可直接复制运行

JAVA
synchronized(lock){
JAVA
// 操作共享资源代码块
JAVA
[修饰符] synchronized 返回值类型 方法名([参数1,……]){}