Lesson13 程序中的并发、多线程程序设计

1 进程的概念

  • 程序(program):静态的代码,是对数据描述与操作的代码的集合,它是应用程序执行的蓝本。
  • 进程(process)是程序的一次执行过程,是系统运行程序的基本单位。程序是静态的,进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程
  • 线程:比进程更小的执行单位,一个进程中可以包含多个线程。
    • 一个进程在执行过程中,为了同时完成多项操作,可以产生多个线程,形成多条执行线索。
    • 每个线程都有它自身的产生、存在和消亡的过程。
  • 一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源
  • 操作系统给每个线程分配不同的CPU时间片,在某一时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行
线程和进程之间的关系
  • 多进程环境中每一个进程既包括其所要执行的指令,也包括执行指令所需的任何系统资源,不同进程所占用的系统资源相对独立
  • 线程是比进程单位更小的执行单位,多线程环境中每一个线程都隶属于某一进程,由进程触发执行,在系统资源的使用上,属于同一进程的所有线程共享该进程的系统资源
  • 与进程不同的是线程本身即没有入口,也没有出口,其自身也不能独立运行,它栖身于某个进程之中,由进程启动运行,完成其任务后,自动终止,也可以由进程使之强制终止。
多线程程序设计

单个程序包含并发执行的多个线程。当多线程程序执行时,该程序对应的进程中就有多个控制流在同时执行,即具有并发执行的多个线程

为什么用多线程?
  • 速度快:线程之间共享相同的内存单元(代码和数据),因此在线程间切换,不需要很大的系统开销,所以线程之间的切换速度远远比进程之间快,线程之间的通信也比进程通信快的多。
  • CPU利用率高:多个线程轮流抢占CPU资源而运行时,从微观上讲,一个时间里只能有一个作业被执行,在宏观上可使多个作业被同时执行,即等同于要让多台计算机同时工作,使系统资源特别是CPU的利用率得到提高,从而可以提高整个程序的执行效率

2 线程的运行

每个线程都有一个独立的程序计数器和方法调用栈(method invocation stack):

  • 程序计数器:程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  • 方法调用栈:简称方法栈,用来跟踪线程运行中一系列的方法调用过程,栈中的元素称为栈桢。每当线程调用一个方法,就会向方法栈压入一个新桢,桢用来存储方法的参数、局部变量和运算过程中的临时数据

  • 栈存简单局部变量,堆存类对象
    Pasted image 20241216161558.png

  • 线程运行中需要的资源:CPU、方法区的代码、堆区的数据、栈区的方法调用栈

线程的调度

线程的调度
  • 在Java中,线程调度通常是抢占式(即哪一个线程先抢到CPU资源则先运行),而不是分时间片式
  • 一旦一个线程获得执行权,这个线程将持续运行下去,直到它运行结束或因为某种原因而阻塞,或者有另一个高优先级线程就绪(这种情况称为低优先级线程被高优先级线程所抢占)。
  • 所有被阻塞的线程按次序排列,组成一个阻塞队列。例如:
    • 因为需要等待一个较慢的外部设备,例如磁盘或用户。
    • 让 处 于 运 行 状 态 的 线 程 调 用Thread.sleep()方法。
    • 让处于运行状态的线程调用另一个线程的join()方法。
  • 所有就绪但没有运行的线程则根据其优先级排入一个就绪队列
  • 当CPU空闲时,如果就绪队列不空,就绪队列中第一个具有最高优先级的线程将运行。
  • 当一个线程被抢占而停止运行时,它的运行态被改变并放到就绪队列的队尾;
  • 一个被阻塞(可能因为睡眠或等待I/O设备)的线程就绪后通常也放到就绪队列的队尾
优先级

线程的调度是按:

  1. 其优先级的高低顺序执行的;
  2. 同样优先级的线程遵循“先到先执行的原则”

线程优先级

  • 范围 1~10 (10 级)。数值越大,级别越高
  • Thread 类定义的 3 个常数:
    • MIN_PRIORITY 最低(小)优先级(值为1)
    • MAX_PRIORITY 最高(大)优先级(值为10)
    • NORM_PRIORITY 默认优先级(值为5)
  • 线程创建时,继承父线程的优先级
  • 常用方法:
    • getPriority( ):获得线程的优先级
    • setPriority( ):设置线程的优先级
主线程
  • main( ) 方法:每当用java命令启动一个Java虚拟机进程( Application 应用程序),Java虚拟机就会创建一个主线程,该线程从程序入口main()方法开始执行
  • 当在主线程中创建 Thread 类或其子类对象时,就创建了一个线程对象。主线程就是上述创建线程的父线程
  • Programmer可以控制线程的启动、挂起与终止。
线程的状态

新建、就绪、运行、阻塞、终止
Pasted image 20241216161908.png

  • 新建:当一个 Thread 类或其子类对象被创建时,新产生的线程处于新建状态,此时它已经有了相应的内存空间和其他资源
    如: Thread myThread=new Thread( );
  • 就绪:调用 start( ) 方法来启动处于新建状态的线程后,将进入线程队列排队等待 CPU 服务,此时它已经具备了运行的条件,一旦轮到它来享用 CPU 资源时,就可以脱离创建它的主线程,开始自己的生命周期
  • 运行:当就绪状态的线程被调度并获得处理器资源时,便进入运行状态。
    • 每一个 Thread 类及其子类的对象都有一个重要的 run( ) 方法,当线程对象被调用执行时,它将自动调用本对象的 run( )方法,从第一句开始顺序执行。
    • run( ) 方法定义了这个线程的操作和功能。
  • 阻塞:一个正在执行的线程暂停自己的执行而进入的状态。引起线程由运行状态进入阻塞状态的可能情况:
    • 该线程正在等待 I/O 操作的完成:等待 I/O 操作完成或回到就绪状态
    • 网络操作
    • 为了获取锁而进入阻塞操作
    • 调用了该线程的 sleep( ) 方法:等待其指定的休眠事件结束后,自动脱离阻塞状态,回到就绪状态
    • 调用了 wait( ) 方法:调用 notify( )或 notifyAll( ) 方法;
    • 让处于运行状态的线程调用另一个线程的join()方法
  • 终止:
    • 自然终止:线程完成了自己的全部工作
    • 强制终止:在线程执行完之前,调用stop( ) 或 destroy( ) 方法终止线程
      Pasted image 20241216163315.png

3 创建和启动线程

Java中的多线程是建立在Thread类,Runnable接口的基础上的,通常有两种办法让我们来创建一个新的线程:

  • 创建一个Thread类,或者一个Thread子类的对象;
  • 创建一个实现Runnable接口的类的对象;
Thread构造方法

Pasted image 20241216163606.png

  • 一个线程的创建肯定是由另一个线程完成的;
  • 被创建线程的父线程是创建它的线程;
  • main线程由JVM创建,而main线程又可以成为其他线程的父线程;
  • 如果一个线程创建的时候没有指定ThreadGroup,那么将会和父线程同一个ThreadGroup。main线程所在的ThreadGroup称为main;
Thread常用方法

Pasted image 20241216163502.png
Pasted image 20241216163515.png
Pasted image 20241216163527.png

tips

因为Java线程的调度不是分时的,所以你必须确保你的代码中的线程会不时地给另外一个线程运行的机会。有三种方法可以做到一点:

  • 让处于运行状态的线程调用Thread.sleep()方法。
  • 让处于运行状态的线程调用Thread.yield()方法。
  • 让处于运行状态的线程调用另一个线程的join()方法。
sleep与yield
  • 这两个方法都是静态的实例方法。
  • sleep()使线程转入阻塞状态,而yield()使线程转入runnable状态。
  • yield()给相同优先级或更高的线程运行机会,如果当前没有存在相同优先级的线程,则yield()什么都不做。而sleep()不会考虑线程的优先级,会给其他线程运行的机会,因此也会给相同或更低优先级线程运行机会。
  • sleep()会有中断异常抛出,而yiled()不抛出任何异常。
  • sleep()方法具有更好的可移植性,因为yield()的实现还取决于底层的操作系统对线程的调度策略。
  • 对于yield()的主要用途是在测试阶段人为的提高程序的并发性能,以帮助发现一些隐藏的并发错误,当程序正常运行时,则不能依靠yield方法提高程序的并发行能。
wait与sleep
  • 方法wait()与sleep() 方法一样,都能使线程等待而停止运行
  • sleep()方法不会释放对象的锁,而wait()方法进入等待时,可以释放对象的锁,因而别的线程能对这些加锁的对象进行操作。
  • 所以,wait,notify和notifyAll都是与同步相关联的方法,只有在synchronized方法中才可以用。在不同步的方法或代码中则使用sleep()方法使线程暂时停止运行
更推荐……

在JDK1.5后,引入了TimeUnit,其中对sleep()方法提供了很好的封装,建议使用TimeUnit.XXXX.sleep去代替Thread.sleep
TimeUnit.SECONDS.sleep(1);
TimeUnit.MINUTES.sleep(3);

join

作用:使当前正在运行的线程暂停下来,等待指定的时间后等待调用该方法的线程结束后,再恢复运行

应用线程类Thread创建线程
  • 将一个类定义为Thread的子类,那么这个类就可以用来创建线程。
  • 这个类中有一个至关重要的方法——public void run,这个方法称为线程体,它是整个线程的核心,线程所要完成任务的代码都定义在线程体中,实际上不同功能的线程之间的区别就在于它们线程体的不同
    Pasted image 20241216164750.png
应用Runnable接口创建线程
  • Runnable是Java中用以实现线程的接口,从根本上讲,任何实现线程功能的类都必须实现该接口。
    • Thread(Runnable target);
    • Thread(Runnable target, String name);
  • Runnable接口中只定义了一个方法就是run()方法,也就是线程体
适用于采用实现Runnable接口方法的情况
  • 避免单继承的局限:因为Java只允许单继承,如果一个类已经继承了Thread,就不能再继承其他类。
  • 特别是在除了run()方法以外,并不打算重写Thread类的其它方法的情况下,以实现Runnable接口的方式生成新线程就显得更加合理了。
  • 涉及到数据共享的时候;
终止线程
  • 当线程执行完run()方法,它将自然终止运行。
  • Thread有一个stop()方法,可以强制结束线程,但这种方法是不安全的。因此,在stop()方法已经被废弃。
  • 实际编程中,一般是定义一个标志变量,然后通过程序来改变标志变量的值,从而控制线程从run()方法中自然退出
总结:创建用户多线程的步骤

法1
Pasted image 20241216165902.png

法2
Pasted image 20241216165931.png

法3
Pasted image 20241216170010.png

  • 在程序开发中只要是多线程尽量以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:
    • 避免单继承的局限,一个类可以实现多个接口。
    • 适合于资源的共享
  • Runnable的局限性
    • run() 方法的返回值是void
    • 不允许抛出任何已检查的异常(编译时捕获的异常)
Callable接口

Java中实现多线程有三种方法:

  • 一种是继承Thread类;
  • 第二种是实现Runnable接口;
  • 第三种是实现Callable接口;
实现callable接口的步骤

Pasted image 20241216170326.png
Pasted image 20241216170429.png

callable接口的特点

Pasted image 20241216170340.png

线程池

日常开发中,推荐使用线程池的方式来使用。最开始创建一堆线程放在池子里,用的时候拿出来用,不用就放回去,能够减少线程的启动和灭亡

callable和runnable
  • Callable 使用 call()方法, Runnable 使用run() 方法
  • Callable的任务执行后可返回值,而Runnable的任务不能有返回值(是void)
  • call() 可以抛出受检查的异常,而run()不能抛出受检查的异常。
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
实际工作中如何选择
  • 取舍的基本原则就是需不需要返回值,如果不需要返回值,那直接就选 Runnable。如果有返回值的话,使用Callable。
  • 另外一点就是是否需要抛出异常, Runnable是不接受抛出异常的,Callable可以抛出异常。
  • Runnable适合那种纯异步的处理逻辑。比如每天定时计算报表,将报表存储到数据库或者其他地方,只是要计算,不需要马上展示,展示内容是在其他的方法中单独获取的。(比如那些非核心的功能,当核心流程执行完毕后,非核心功能就自己去执行)
  • Callable适用于那些需要返回值或者需要抛出checked exception的情况,比如对某个任务的计算结果进行处理。在Java中,常常使用callable来实现异步任务的处理,以提高系统的吞吐量和响应速度
Built with MDFriday ❤️