并发编程一、多线程基础

前言:

  1. 文章内容:线程与进程、线程生命周期、线程中断、线程常见问题总结
  2. 本文章内容来源于笔者学习笔记,内容可能与相关书籍内容重合
  3. 偏向于知识核心总结,非零基础学习文章,可用于知识的体系建立,核心内容复习,如有帮助,十分荣幸
  4. 相关文献:并发编程实战、计算机原理

线程与进程的关系


多个角度掌握进程与线程的区别:

  • 粒度:进程是OS资源分配的基本单位,线程是CPU调度和执行的基本单位
  • 内存:进程占有一定的内存空间,且进程之间内存隔离,同一个进程下的线程共享该进程的内存空间
  • 稳定:一个进程出现问题不影响其他进程,一个线程崩溃可能影响整个进程的稳定性
  • 消耗:进程的创建和销毁需要保持寄存器和栈信息,还要进行资源的分配回收和页调度,开销较大。线程只需要保存寄存器和栈信息,开销较小。

扩展一下为何进程的开销很大:

  • 线程的创建时间⽐进程快,因为进程在创建的过程中,还需要资源管理信息,⽐如内存管理信息、⽂件管理信息,⽽线程在创建的过程中,不会涉及这些资源管理信息,⽽是共享它们
  • 线程的终⽌时间⽐进程快,因为线程释放的资源相⽐进程少很多
  • 同⼀个进程内的线程切换⽐进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同⼀个进程的线程都具有同⼀个⻚表,那么在切换的时候不需要切换⻚表。⽽对于进程之间的切换,切换的时候要把⻚表给切换掉,⽽⻚表的切换过程开销是⽐较⼤的
  • 由于同⼀进程的各线程间共享内存和⽂件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更⾼了

线程的生命周期


线程的生命周期:

  • New(新建):
    • 线程在编程语言层面被创建,JVM分配内存并初始化成员变量,不允许分配CPU执行
  • Runable(就绪):
    • 线程对象调用start方法之后,线程处于就绪状态,jvm会为其创建方法调用栈和程序计数器,等待分配CPU执行。
    • 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
    • 锁池里的线程拿到对象锁后,进入就绪状态。
  • Running(运行状态):
    • 处于就绪状态的线程获得了CPU,开始执行run方法里的方法体,线程处于运行状态
  • Blocked(阻塞):
    • 处于运行状态的线程失去所占用资源后,进入阻塞状态。进入synchronized关键字修饰的方法或代码块(获取锁)之前时的状态。同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,线程进入_EntryList:存放被阻塞线程的队列
  • Waiting(等待状态):
    • 运行的线程执行wait()方法,线程进入_WaitSet中等待被唤醒,调用sleep、join、park也进入等待
  • Waiting_Timeout(超时等待):
    • 等待的方法加了超时时间,等待被唤醒或超时自动唤醒。
  • Terminated(线程终止):
    • 线程生命终止,异常中断或执行完毕。

创建线程的方法:

  1. 继承Thread并重写run方法,调用start方法启动,等线程第一次获得时间片时再调用run方法。
  2. 实现Runnable接口并实现run方法,任务线程无返回值
  3. 实现Callable接口并实现call方法,Callable接口的任务线程能返回执行结果,call方法可以抛出异常
  4. 线程池或者定时器工具类。

创建线程的方式对比:

  • Runnable接口:
    • 可以让具体任务的run和Thread类解耦。一个任务实现Runnable接口,然后把对应实例传给Thread类就可以。让同一个任务类传给不同的Thread,任务类也不负责创建线程等工作。
    • 继承Thread类的话,每次要新建一个任务,只能新建一个独立线程。这样损耗比较大,使用Runnable或线程池可以减小损耗。
    • 继承Thread类,由于java不支持多继承,这样就无法继承其他类,限制了可扩展性
  • Thread类:
    • 在run方法内部获取当前线程直接用this,无须使用Thread.currentThread方法
    • 继承的类的内部变量不会直接共享,少数不需要共享变量的场景下使用更方便

线程中断的方法


如何停止线程:

  1. 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。标志用volatile修饰 保证其他线程读取的总是最新值
  2. 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。
    1. 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
  3. 使用 interrupt 方法中断线程。

stop与interrupt区别:

  1. stop杀死线程,当线程持有ReentrantLock锁,被stop的线程不会自动释放锁,其他线程再也没机会获得锁。
  2. interrupt是通知,告知线程可以结束但不会结束线程的运行。通过修改线程的interrupt标志位,让线程自行去读取标志位,自行判断是否需要中断。

通过interrupt方法中断线程:

  1. 线程处于sleep/wait/join/await状态,调用interrupt方法,那么线程会立即退出阻塞并抛出InterruptException异常。主动捕获异常来进行中断。
  2. 线程处于I/O阻塞,将抛出ClosedByInterruptException,捕获异常并进行中断处理。
  3. 线程调用interrupt方法,通过Thread.interrupted来判断是否发生中断,如果检测到了中断标志位,就不继续执行逻辑而是中断线程。

interrupt、interrupted、isInterrupted方法及区别:

  • interrupt():在当前线程中打一个停止标记,并不是真正的停止线程。需要自己监视线程状态并处理。
  • interrupted():判断当前线程是否已经中断,运行后会清除中断状态,是静态方法。
  • isInterrupted():判断调用该方法的线程是否已经中断,是实例方法。
  • interrupted和isInterrupted底层调用是同一个方法,只是参数不同。interrupted参数是true,代表要清除状态位,而isInterrupted是false,代表不清除状态位。

中断使用场景:

  1. 某个操作超过一定的执行时间限制需要中止
  2. 一组线程中一个或多个出现错误导致其他线程都需要取消
  3. 多个线程做相同的事情,只要一个线程成功其他线程都可以取消
  4. 当线程需要终止

常见的多线程基础知识整合


 wait/notify/sleep/yield/join的解释:

  • wait:挂起当前线程,线程进入等待阻塞状态,直到notify或notifyAll唤醒,wait方法会释放锁。
  • notify:唤醒对应对象monitor上等待的线程,notifyAll唤醒所有线程。
  • sleep:让当前线程暂停指定时间,线程会暂时让出CPU执行权,并不释放锁。
  • yield:将运行中的线程变为就绪状态,其他线程有机会执行,当前线程也可能又立即获得时间片,不保证其他线程立马获得cpu时间片。
  • join:父线程等待子线程执行完后再执行。

Join的原理:

        假设主线程调用了 threadA.join, 那么主线程就持有threadA这个synchronized同步锁,那么等join方法返回的时候就退出了同步锁,那么退出同步锁就会唤醒阻塞在synchronized的主线程。

线程上下文切换过程:

        每个线程都有一个程序计数器,一组寄存器,堆栈。寄存器是CPU内部的数量较少但是速度很快的内存。寄存器通过对常用值的快速访问来提高计算机程序运行的速度。程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。

  1. 挂起当前任务(线程/进程),将这个任务在 CPU 中的状态(上下文)存储于内存中的某处
  2. 恢复一个任务(线程/进程),在内存中检索下一个任务的上下文并将其在 CPU 的寄存器中恢复
  3. 跳转到程序计数器所指向的位置(即跳转到任务被中断时的代码行),以恢复该进程在程序中

线程上下文切换消耗原理:

  • 直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉
  • 间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小

使用多线程的原因:

    • 核心就是多线程在某些场景下应用,可以降低延迟(发出请求到收到响应这个过程的时间),提高吞吐量(单位时间内能处理请求的数量)
    • 多线程就是提升I/O设备和CPU综合利用率,将硬件性能发挥到极致
    • 单个线程执行,无法同时操作CPU和I/O。这样总会有一方是空闲,利用率是50%。如果两个线程那么一个利用CPU的时候一个就可以利用I/O。这样I/O与CPU都被充分利用,利用率就是100%。单核多线程切换会增加消耗,而在多核下多线程的原因利用提高CPU利用率。
posted @ 2022-08-30 09:59  难得  阅读(81)  评论(0编辑  收藏  举报