当前位置: 首页 > news >正文

第十一讲 多线程

多线程是提升程序性能非常重要的一种方式,也是Java编程中的一项重要技术。在程序设计中,多线程就是指一个应用程序中有多条并发执行的线索,每条线索都被称作一个线程,它们会交替执行,彼此间可以进行通信。

1. 进程与线程

进程是指正在运行的程序,是系统进行资源分配和调度的基本单位。为了有效利用系统资源和提高运行效率,在一个进程中还可以有若干同时并发运行的线程。每一个进程都至少存在一个线程。

当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上运行main()方法中的代码,这样的程序称为单线程程序。单线程程序中只有一个线程在运行,效率相对较低。好比是售票大厅只开设一个售票窗口,所有人只能在一个窗口排除买票,整个售票过程效率较低;如果同时开设多个售票窗口售票,则可以提高售票效率。编写的程序也是同样,可以通过创建多线程程序来提高程序运行效率。

2. 线程的创建

Java中提供了三种方式来实现多线程。

(1)Thread类实现多线程

Thread类是java.lang包下的一个线程类,可通过继承Thread类的方式来实现多线程,其使用方法是先创建一个Thread线程类的子类(子线程),同时重写Thread类的run()方法;然后创建该子类的实例对象,并通过调用start()方法启动线程。

【例11-1】通过Thread类实现多线程

运行结果如下图所示。

通过运行结果可以看到,两个线程对象交互执行了各自重写的run()方法。并不是按顺序先执行完第一个线程再执行第二个线程。

(2)Runnable接口实现多线程

虽然可以通过继承Thread类实现多线程,但这种使用方式有一定的局限性。因Java只支持单继承,如果某个类已经继承了其他类,就无法再继承Thread类来实现多线程。这时可通过实现Runnable接口的方式来实现多线程。

①使用实现Runnable接口的方式来实现多线程的主要过程如下。

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

③创建Runnable接口的实现类对象。

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

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

【例11-2】通过实现Runnable接口的方式来实现多线程

运行结果如下图所示。

Callable接口实现多线程

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

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

通过Callable接口实现多线程的过程如下。

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

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

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

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

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

【例11-3】通过Callable接口实现多线程

运行结果如下图所示。

Callable接口方式实现的多线程是通过FutureTask类来封装和管理返回结果的,该类的直接父接口是RunnableFuture。FutureTask类的继承关系如下图所示。

FutureTask本质是Runnable接口和Future接口的实现类,而Future则是用来管理线程执行返回结果的。其中Future接口中有5个方法来对线程结果进行管理,如表1所示。

表1 Future接口的方法

方法声明

功能描述

boolean cancel(boolean

mayInterruptIfRunning)

用于取消任务,参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行的任务

boolean isCancelled()

判断任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true

boolean isDone()

判断任务是否已经完成,若任务完成,则返回true

V get()

用于获取执行结果,这个方法会发生阻塞,一直等到任务执行完毕才返回执行结果

V get(long timeout, TimeUnit unit)

用于在指定时间内获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null

 

3. 线程的生命周期

在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当线程任务中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。线程的整个生命周期分为6个状态,分别是NEW(新建状态)、RUNNABLE(可运行状态)、BLOCKED(阻塞状态)、WAITING(等待状态)、TIMED_WAITING(定时等待状态)和TERMINATED(终止状态)。线程的不同状态表明了线程当前正在进行的活动。程序中,通过一些操作,可以使线程在不同状态之间转换,如下图所示。

(1)NEW(新建状态)

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

(2)RUNNABLE(可运行状态)

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

RUNNABLE内部状态转换:

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

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

(3)BLOCKED(阻塞状态)

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

线程一般会在以下两种情况时进入阻塞状态:

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

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

(4)WAITING(等待状态)

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

处于等待状态中的线程不能立即争夺CPU使用权,必须等待其他线程执行特定的操作后,才有机会争夺CPU使用权。例如调用wait()方法而处于等待状态中的线程,必须等待其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程;调用join()方法而处于等待状态中的线程,必须等待其他加入的线程终止。

(5)TIMED_WAITING(定时等待状态)

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

处于定时等待状态中的线程不能立即争夺CPU使用权,必须等待其他相关线程执行完特定的操作或者限时时间结束后,才有机会再次争夺CPU使用权。例如,调用了wait(long timeout) 方法而处于等待状态中的线程,需要通过其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。

(6)TERMINATED(终止状态)

当线程的run()方法、call()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入终止状态。一旦进入终止状态,线程将不再拥有运行的资格,也不能再转换到其他状态,生命周期结束。

4. 线程的调度

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

线程调度有两种模型,分别是分时调度模型和抢占式调度模型。分时调度是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU时间片。抢占式调度是指让可运行池中所有就绪状态的线程争抢CPU的使用权,而优先级高的线程获取CPU执行权的概率大于优先级低的线程。

Java虚拟机默认采用抢占式调度模型,多数情况下不需要去关心它,在某些特定的需求下需要改变这种模式时可由程序来控制 CPU的调度。

(1)设置线程的优先级

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

线程的优先级用1~10之间的整数来表示,数字越大优先级越高。除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量(如表2所示)表示线程的优先级。

表2 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类的三个静态常量。

【例11-4】线程优先级的设置

运行结果如下图所示。

说明:

虽然Java提供了10个线程优先级,但是这些优先级需要操作系统的支持,不同的操作系统对优先级的支持是不一样的,不能很好地和Java中线程优先级一一对应。

(2) 线程休眠

如果想要人为地控制线程执行顺序,使正在执行的线程暂停,将CPU使用权让给其他线程,这时可以使用静态方法sleep(long millis)。该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态,这样其他的线程就可以得到执行的机会。sleep(long millis)方法会声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。

【例11-5】线程休眠

运行结果如下图所示。

5. 多线程同步

多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。如下例中的多线程售票程序。

【例11-6】多线程售票

运行结果如下图所示。

由运行结果可以看到,同一张票被出售了多次,这种现象是不应该出现的。出现这种问题的原因在于多个线程同时处理共享资源所导致的。为了解决这样的问题,只需要保证某个资源在同一时刻只能被一个线程访问即可,也即线程的同步,Java中提供了几种不同的线程同步机制。

(1)同步代码块

当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个使用synchronized关键字修饰的代码块中,这段代码块就被称为同步代码块。使用格式如下。

     synchronized(lock){

             // 操作共享资源代码块 ...

            }

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

【例11-7】利用同步代码块实现线程同步

运行结果如下图所示。

同步代码块的原理:

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

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

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

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

(2)同步方法

当把共享资源的操作放在同步代码块中时,便为这些操作加了同步锁。同样,也可以在方法前面使用synchronized关键字来修饰,被修饰的方法称为同步方法,可实现和同步代码块同样的功能,同步方法使用格式如下所示。

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

//方法体

}

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

【例11-8】使用同步方法实现线程同步

运行结果如下图所示。

 

http://www.lryc.cn/news/524973.html

相关文章:

  • VUE之路由Props、replace、编程式路由导航、重定向
  • windows安装ES
  • 论文速读|Multi-Modal Disordered Representation Learning Network for TBPS.AAAI24
  • 小哆啦解题记:加油站的奇幻冒险
  • 【前端】CSS实战之音乐播放器
  • Games104——渲染中光和材质的数学魔法
  • impala增加字段,hsql查不到数据
  • SpringBoot项目中的异常处理
  • ComfyUI实现老照片修复——AI修复老照片(ComfyUI-ReActor / ReSwapper)尚待完善
  • NLTK命名实体识别(NER)
  • 【游戏设计原理】78 - 持续注意力
  • Android设备:Linux远程lldb调试
  • 多层 RNN原理以及实现
  • [Computer Vision]实验三:图像拼接
  • 【Vim Masterclass 笔记22】S09L40 + L41:同步练习11:Vim 的配置与 vimrc 文件的相关操作(含点评课内容)
  • 5.9 洞察 OpenAI - Translator:日志(Logger)模块的 “时光记录仪”
  • 客户案例:电商平台对帐-账单管理(亚马逊amazon)
  • IP协议特性
  • Kubernetes入门学习
  • 支持向量机SVM的应用案例
  • Chrome 132 版本新特性
  • (5)STM32 USB设备开发-USB键盘
  • Linux 系统服务开机自启动指导手册
  • 分布式多卡训练(DDP)踩坑
  • Codeforces Round 1000 (Div. 2)-C题(树上两个节点不同边数最大值)
  • C++17 新特性解析:Lambda 捕获 this
  • Spring Boot 使用 Micrometer 集成 Prometheus 监控 Java 应用性能
  • Spring Boot 事件驱动:构建灵活可扩展的应用
  • IM系统设计
  • 华为EC6110T-海思Hi3798MV310_安卓9.0_通刷-强刷固件包