使用工具 IntelliJ IDEA Community Edition 2023.1.4
使用语言 Java8
代码能力快速提升小方法,看完代码自己敲一遍,十分有用
目录
1.多线程概述
世间万物中每个个体都可以同时完成很多工作。例如,人体可以同时进行呼吸、血液循环、思考问题等活动。用户既可以使用计算机听歌,也可以编写文档、发送文件等,这些活动可以同时进行。在计算机领域里,这种同时执行多个操作的行为模式被称为并发。在操作系统中同时运行着多个独立的任务,每个任务对应一个进程,每个进程可产生多个线程。通过这种并发运行的方式,计算机的性能被挖掘到了极限。
1.1 进程与线程
进程是程序的一次动态执行过程,它是从代码加载、执行中到执行完毕的一个完整过程,也是进程本身从产生、发展到最终消亡的过程。操作系统同时管理一个计算机系统中的多个进程,让计算机系统中的多个进程轮流使用中央处理器(Central Processing Unit,CPU)资源,或者共享操作系统的其他资源。由于CPU执行速度非常快,所有程序好像是在"同时"运行一样。
在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程(由用户程序建立的进程)。可以从Windows任务管理器中查看已启动的进程,进程是系统运行程序的最小单元。各进程之间是独立的,每个进程的内部数据和状态也是完全独立的。
线程是进程中执行运算的最小单位,是在进程基础上的进一步划分,一个线程可以完成一个独立的顺序控制流程。下面看一个简单的问题,假设一个水箱有五个排水孔,打开一个排水孔清空水箱要一个小时,怎样才能使水箱迅速清空并计算最快的清空事件?如果把水箱排水比作一个进程,那么一个排水孔就是一个线程。
与进程不同,同一进程内的多个线程共享同一块内存空间(包括代码空间、数据控件)和一块系统资源,所以系统在产生一个线程或在各线程之间切换工作时,其负担要比在进程间切换小得多。
综上所述,进程和线程是两个不同的概念。应用程序有单进程单线程的,有多进程但每个进程只有一个线程的,有单进程包含多线程的,还有多进程且每个进程都有多个线程等四种情况。
如果在同一个进程中同时有多个线程,用于执行不同的工作,被称之为"多线程"。这些线程可以同时存在、同时执行。例如,只有一个排水孔的水箱可以比作单线程程序,有多个排水孔的水箱可比作多线程程序。
1.2 多线程的运行机制
以往开发的程序大多是多线程的,即一个程序只有从开始到结束这一条执行路径。而多线程是指一个进程同时存在几条执行路径且并发执行的工作方式。
并发运行与并行运行不同。并行运行通常表示同一个时刻有多条指令代码在处理器上同时运行,这种情况往往需要多个处理器支持。而并发运行表示在一个处理器中,操作系统为了提高程序的运行效率,将CPU的执行时间分成多个时间片,分配给同一进程的不同线程。当执行完一个时间片后,当前运行的线程就可能交付出CPU权限,让其他线程执行下一个时间片,当然CPU也有可能将相邻的时间片分配给同一线程,即多个线程分享CPU时间,交替运行。之所以从表面上看是多个线程同时运行的,是因为不同线程之间切换的时间非常短,也许仅仅是几毫秒,对普通人来说是难以感知的,即所谓的"宏观并行,微观串行" 。
由于进程和线程含义不同,所以运行多个进程和多个线程是不同的。运行多个进程指在同一段时间内,可以同时运行一个以上的程序,即多个任务同时执行。例如,一边浏览网页,一边听音乐。而运行多个线程指在同一个程序内同时运行一个以上的子程序,如使用网盘同时下载多个文件。
1.3 多线程的优势
多线程作为一种多任务并发的工作方式,有着广泛的应用。合理使用线程,将减少开发和维护的成本,甚至可以改善复杂应用程序的性能。使用多线程的优势如下:
优势
- 充分利用CPU的资源。运行单线程程序时,若程序发生阻塞,则CPU可能会处于空闲状态,这将造成计算机资源浪费。而使用多线程可以在某个线程处于休眠或阻塞状态时运行其他线程,这将大大提高资源利用率。
- 简化编程模型。可以考虑将一个既长又复杂的进程分为多个线程,成为几个独立执行的模块,如使用时、分、秒来描述当前时间。如果时单线程程序,则需要多重判断。如果使用多线程,时,分,秒各使用一个线程控制,每个线程仅需实现简单的流程,简化了程序逻辑,更方便编码和维护。
- 良好的用户体验。由于有多个线程可以交替运行,减少或避免了程序阻塞或意外情况造成的响应过慢现象,减少了用户等待时间,提升了用户体验。
多线程技术在实际开发中时非常有价值的。例如,Word文本编辑工具需提供一边编辑一边保存的功能,且同时进行规范化格式和标记错别字检查;应该浏览器必须能同时下载多个图片;一个Web服务器必须能同时响应多个用户请求;JVM本身就在后台提供了一个超级线程进行垃圾回收.......总之,多线程在实际编程中的应用是非常广泛的。
2.多线程编程
Java语言提供了java.lang.Thread类支持多线程编程;
2.1 Thread类介绍
Thread类提供了大量的方法来控制和操作线程。Thread类的常用方法如下:
Thread() | 创建Thread对象 | 构造方法 |
Thread(Runnable target) | 创建Thread对象,target为run()方法被调用的对象 | 构造方法 |
Thread(Runnable target,String name) | 创建Thread对象,target为run()方法被调用的对象,name为新线程的名字 | 构造方法 |
void run() | 执行任务操作的方法 | 实例方法 |
void start() | 使该线程开始运行,JVM将调用该线程的run方法 一般使用这个,不直接调用run方法 | 实例方法 |
void sleep() | 在指定的毫秒数内让当前正在运行的线程休眠(暂停运行) | 静态方法 |
Thread currentThread() | 返回当前线程对象的引用 | 静态方法 |
Thread类的静态方法currentThread()方法返回当前线程对象的引用。在Java程序启动时,一个线程立即随之启动,这个线程通常被称为程序的主线程。public static void main()方法是主线程的入口,每个进程至少有一个主线程。它的重要性如下:
- 主线程是产生其他子线程的线程
- 主线程通常必须最后完成运行,因为它执行各种关闭工作
尽管主线程是自动创建的,但是可以由一个Thread对象控制。因此,可以使用Thread类的方法获取主线程信息。
在Java语言中,实现多线程的方式有两种:一种是继承Thread类,另一种是实现Runnable接口。
2.2 继承Thread类创建线程类
继承Thread类是实现线程的一种方式。在使用此方法自定义线程类时,必须在格式上满足以下要求:
- 此类必须继承Thread类
- 将线程执行的代码写在run()方法中
线程从它的run方法开始执行,即run()方法时线程执行的起点,就像main()方法是应用程序的起点一样。因为run()方法定义在Thread类中,所以在自定义线程类中必须重写run()方法,为线程提供实现具体任务的代码。
2.2.1 Thread类贯穿示例
示例代码
运行结果
后面的循环内容将会每隔2秒打印一句
注意:
1.已启动的线程对象(就是同一个线程对象)不能重复调用start()方法,否则会抛出IllegalThreadStateException异常。
2.sleep()方法用来控制线程的休眠时间。如果这个线程被其他线程中断,则会产生InterruptedException异常。所以sleep()方法必须进行异常处理。
线程对象调用start方法和调用run方法截然不同。前者是启动线程,后者是调用实例方法,在实际应用中切不要混淆。
2.3 实现Runnable接口创建线程类
在使用继承Thread类的方式创建线程的过程中,子类无法再继承其他父类。这是因为Java语言不支持多重继承。在这种情况下,可以通过实现Runnable接口的方式创建线程。这种方式更具有灵活性,用户线程还可以通过继承,再具有其他的特性,这是开发中经常使用的方式。
Runnable接口位于java.lang包中,其中只提供一个抽象方法run()的声明,Thread类也实现了Runnable接口。使用Runnable接口时也列表框Thread类,这是因为它要用到Thread类中的start()方法。在Runnable接口中只有run()方法,其他操作都要借助于Thread类。使用Runnable接口创建线程的一般格式如下。
2.3.1 Runnable接口贯穿示例
示例代码
运行结果
后面的循环内容将会每隔2秒打印一句
在以上代码中,虽然TestRunnable类为线程提供了run()方法,但它本身不是线程类。如果创建一个专门执行run()方法的线程对象,则需要创建一个Thread对象。
TestRunnable类实现了Runnable接口,在run()方法中编写线程所执行的代码。如果TestRunnable需要继承其他类,也完全可以实现。
3.线程的状态转换
3.1 状态转换
Java语言使用Thread类及其子类的对象表示线程,新建的线程通常会在五种状态中转换,即新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)。这五种状态组成了线程的生命周期,如下所示:
3.2 状态转换详解
新建状态。一个Thread类或其子类的对象被声明并创建,但其还在未启动的这段时间里处于一种特殊的新建状态中。此时,线程类对象已经被分配了内存空间和资源,并且已被初始化,但是该线程尚未被调度;
就绪状态。就绪状态也被称为可运行状态,处于新建状态的线程被启动后,也就是调用start()方法后,新建线程将进入线程队列,排队等待CPU时间片。此时它已具备运行的条件,一旦轮到它使用CPU资源,他就可以脱离创建它的主线程独立开始它的生命周期。这就好比一批作物种子被运输到农场,处于等待果农撒种的状态。
运行状态。当就绪状态的进程被调度并获得处理器资源后便进入运行状态,该状态表示线程正在运行,该线程已经拥有了CPU的占用券。这就好比作物种子被农夫种植到土地里,在阳光的照耀和雨水的浇灌下生根、发芽、茁壮成长。
当调用start()方法后,线程获取资源运行run()方法中的代码,代码进入运行状态。每个Thread类及其子类都拥有一个线程操作方法run(),当线程类对象被调度运行时,它将自动调用此run()方法,从该run()方法的第一条语句开始执行,一直到执行完毕。处于运行状态的线程在以下四种情况下会让出CPU的控制权。
- 线程运行完毕
- 有比当前线程优先级更高的线程抢占了CPU
- 线程休眠
- 线程因等待某个资源而处于阻塞状态
阻塞状态。一个正在运行的线程在某些特殊情况下需要让出CPU并暂时中止运行。这时,线程处于不可运行的状态被称为阻塞状态。这就好比作物在生长过程中由于干旱少雨或遭遇虫害停滞生长。
导致线程死亡的原因有两个:一时正常运行的线程完成了它的全部工作,即运行完run()方法的最后一条语句并退出;二时当进程停止运行时,该进程中的所有线程将被强行中止。线程处于死亡状态并且没有该线程的引用是,JVM会从内存中删除该线程类对象。
简而言之,忽略操作系统底层的操作,在程序中可视的线程生命周期是新建线程、start()方法启动线程、调用run()方法运行线程,运行完毕后线程的生命周期就结束了。
3.3 状态转换贯穿示例
示例代码
运行结果