深入理解Java线程

深入理解Java线程

进程和线程

  • 进程
    • 进程是操作系统资源分配的最小单位
    • 问题: 进程之间是如何通信的,有哪些方式
      • 管道以及有名管道
      • 信号
      • 信号量
      • 消息队列
      • 共享内存,比如实现分布式锁
      • 套接字
  • 线程
    • 线程是操作系统线程调度和执行的最小单位,而线程归属于进程
    • 问题 Java线程之间如何通信的,有哪些方式
      • volatile保证线程之间共享变量的可见性
      • 管道输入输出流: PipedWriter、PIpedReader
      • join: 基于等待唤醒机制
    • 问题: 线程的同步和互斥
      • 线程同步: 线程之间存在一种关系,一个线程需要等待另外一个线程的消息之后才能进行,否则就需要等待
      • 线程互斥: 对于共享资源只能线程独享,想要获取需要等待另外一个线程释放资源
  • 问题: 线程和进程之间的区别
    • 线程更轻量级,线程的上下文切换成本比进程上下文切换成本低
    • 进程间的通信比较复杂,线程之间的通信比较简单比如共享进程内的内存
    • 进程是操作系统资源分配的最小单位,线程是操作系统线程调度和执行的最小单位,而线程归属于进程
  • 问题: 四种线程同步互斥的控制方法
    • 临界区: 通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问(在一段时间内只允许一个线程访问的资源就称为临界资源)
    • 互斥量: 为协调共同对一个共享资源的单独访问而设计的
    • 信号量: 为控制一个具有有限数量用户资源而设计
    • 事件: 用来通知线程有一些事件已发生,从而启动后继任务的开始

上下文切换

  • 问题: 什么是上下文切换
    • 上下文切换是指CPU从一个进程或线程切换到另外一个线程或者进程,上下文切换会保存上一次的状态,以便于下一次继续执行
    • 上下文切换只发生在内核态
    • 上下文切换耗费时间成本比较大,尽量避免
  • 问题: 上下文切换的步骤
    • 暂停当前线程的处理,将当前线程的上下文保存下来,执行下一个线程的处理直到时间片用完暂停,再通过之前保存的上下文去继续执行之前线程的处理
  • 问题: 造成CPU上下文切换的方式
    • 进程和线程的切换
    • 系统调用
    • 中断机制

内核模式和用户模式

image.png
  • 问题: 什么是内核模式和用户模式
    • 内核模式(内核态)
      • 内核态,执行代码可以完全不受限制的访问底层硬件
    • 用户模式(用户态)
      • 用户态执行代码不能直接访问底层硬件,需要通过系统调用
  • 问题: CAS操作是否涉及到用户态到内核态的切换
    • CAS不会涉及到用户态到内核态的切换,CAS在多核处理器下相当于在代码里插入了lock cmpxchgl指令来保证原子性,而且执行指令比上下文切换开销小,所以CAS比互斥锁性能更高

操作系统层面线程的生命周期

  • 操作系统层面线程的生命周期
    • 初始状态: 线程已经被创建,但是还不允许CPU执行
    • 就绪状态: 线程可以分配给CPU执行
    • 运行状态: 当CPU空闲的时候,会将它分配到一个就绪状态的线程去使用
    • 休眠状态: 运行状态的线程调用阻塞的API时会进入阻塞状态等待被唤醒继续运行
    • 终止状态: 线程执行结束或遇到异常
    • 小结
      • 线程一开始被创建时进入初始状态,然后可以被分配给CPU时处于就绪状态,当CPU空闲的时会从就绪状态的线程中挑选一个线程去执行进入运行状态,当运行状态下的线程调用阻塞API时会进入阻塞状态等待被唤醒继续运行,当线程执行完或被异常停止处于终止状态

Java层面线程的生命周期

image.png

  • Java层面线程的生命周期
    • NEW(初始化状态)
    • RUNNABLE(就绪状态 + 运行状态 = 可运行状态)
    • BLOCKED(阻塞状态): 只有synchronized使用
    • TIMED_WAITING(有时限等待状态): 调用wait()方法时指定等待的时长就会处于此状态
    • TERMINATED(终止状态)
  • 问题: 概括的解释下线程的几种状态
    • 新建(new)
      • 新创建一个线程对象
    • 可运行(runnable)
      • 线程对象创建后,其他线程比如main线程调用了该对象的start方法,该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权
    • 运行(running)
      • 可运行状态的线程获得了cpu时间片,执行程序代码
    • 阻塞(block)
      • 阻塞状态就是指线程因为某种原因放弃了cpu使用权,也就是让出了cpu时间片,暂时停止运行,直到线程进入可运行状态,才有机会获得cpu时间片转到运行状态
      • 阻塞的情况分三种
        • 等待阻塞: 运行的线程执行wait方法,JVM会把线程放入等待队列中
        • 同步阻塞: 运行线程在获取对象的同步锁的时候,如果锁没被释放,JVM会把该线程放入锁池中
        • 其他阻塞: 运行的线程执行sleep、join、发送IO请求时,JVM会把线程变为阻塞状态,当sleep超时、join等待线程终止或超时、IO处理完毕时,线程重新转成可运行状态
      • 死亡(dead): 线程run、main方法执行结束、异常退出run,就代表该线程生命周期结束,死亡的线程不可再次复生

Java线程

Java线程概述

image.png

  • Java线程属于内核级线程,是依赖于内核的也就是无论是用户进程中的线程还是系统进程中的线程,它们的创建、撤销、切换都是需要切换到内核态去执行
  • 问题: 为什么说创建Java线程的方式本质只有一种
    • 虽然说创建线程的方式有以下几种
      • 继承Thread类实现run方法
      • 实现Runable接口,实现run方法
      • 实现Callable接口,实现call方法
      • 通过线程池去创建线程: 推荐使用
    • 但是本质只有一种,都是通过new Thread创建线程,调用Thread.start启动线程,最终都会去调用Threead.run
  • 问题: Java线程和Go的协程有什么区别
    • 什么是协程
      • 协程是基于线程之上但是又更加轻量级的存在,协程存在于用户态,不被操作系统内核管理
    • 如果线程不用切换到内核态,开销非常小,就可以创建多个用户级别来执行任务,这样并发量特别高,所以Go天生就是和做这种大量并发的场景
  • 问题: Java线程执行为什么不能直接调用run方法,而要调用start方法
    • 因为run方法并不是真正的线程,只是普通对象的方法,而start方法会通过操作系统去创建线程需要切换到内核态,Java线程的创建和销毁是个比较重的操作,因为涉及到内核态切换,所以我们一般不会每一个任务分配一个线程而是选择线程复用的方式比如使用线程池

Thread的常用方法

  • sleep
    • 调用sleep会让当前线程从RUNNING进入TIMED_WAITING,不会释放对象锁
    • 其他线程可以通过interrupt方法打断正在睡眠的线程,sleep方法会抛出终端异常并且清除中断标志
    • 睡眠结束后的线程未必立刻得到执行
    • sleep传入参数为0时和yield相同
  • yield
    • yield会释放CPU资源,让当前线程从RUNNING进入RUNNABLE状态,让优先级更高的线程获得执行机会,不会释放对象锁
    • 假设当前进程只有main线程,当调用yield之后,main线程会继续运行,因为没有比它优先级更高的线程
    • 具体的实现依赖于操作系统的任务调度
  • join
    • 等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结果之后才能继续运行的场景
    • 注意
      • 可以理解为线程合并,当在一个线程调用另外一个线程的join时,当前线程阻塞等待被调用join的线程执行完毕才能继续执行,所以join的好处就是能够保证线程的执行顺序,但如果调用线程的join方法其实已经失去了并行的意义,虽然存在多个线程,但本质上是串行的,最后join底层也是采用等待唤醒机制
  • stop
    • stop方法会释放对象锁,可能会造成数据不一致,因为stop方法太暴力,会强行把执行到一半的线程终止

Java线程的实现原理

  • 线程创建和启动流程

    • 使用new Thread()创建一个线程,然后调用start()方法进行java层面线程启动

    • 使用本地方法start0(),去调用JVM中的JVM_StartThread()方法创建和启动

    • 调用new JavaThread(&thread_entry,sz)进行线程的创建,并根据不同的操作系统平台调用对应os::create_thread()方法进行线程创建

    • 新创建的线程状态是initialized,调用了sync->wait()的方法进行等待,等到被唤醒才继续执行thread->run()

    • 调用Thread.start(native_thread)方法进行线程启动,此时将线程状态设置为RUNNABLE,接着调用os::start_thread(thread),根据不同的操作系统选择不同的线程启动方式

    • 线程启动之后状态设置为RUNNABLE,并且唤醒第四步中等待的线程,接着执行thread->run()方法

    • JavaThread::run()方法会回调第一步的new Thread()中复写的run()方法

Java线程的调度机制

  • 协同式线程调度: 线程执行时间由线程本身控制,但缺点是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那
  • 抢占式线程调度: 无法控制CPU时间片在哪停止,且线程的切换不由线程本身决定,Java默认就是抢占度调度
  • 注意
    • 轮循调度优点是简洁性,它无序记录当前所有连接的状态,所以它是一种无状态调度
    • 抢占式调度实现相对复杂

Java线程的中断机制

  • Java没有提供一种安全、直接的方法来停止某个线程,而是提供了中断机制
  • 中断机制是一种协作机制,也就是说通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理
  • 被中断的线程拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止
  • API的使用
    • interrupt(): 将线程的中断标志位设置为true,不会停止线程
    • isInterrupted(): 判断当前线程的中断标志位是否为true,不会清除中断标志位
    • Thread.interrupted():判断当前线程的中断标志位是否为true,并清除中断标志位,重置为fasle
  • 问题: 如何优雅的终止线程
    • stop会释放锁,强制终止线程,不推荐使用
    • 可以通过while配合isInterrupted方法以及对应的结束标记来使用,注意如果代码块中有调用清除中断标记为的API时,如果使用了sleep、wait记得手动添加标记位

等待唤醒机制

  • 等待唤醒机制可以基于wait和notify方法来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被唤醒
    • park/unpark
      • 一般使用这种,可以唤醒指定线程,unpark提前去掉也是可以的
    • wait/notify/notifyAll
      • Monitor机制去提供,只作用于synchronized同步块,而且无法唤醒指定线程,而unpark可以指定线程,notify不可提前调用
      • notify()是随机性的,只随机唤醒一个 wait 线程
      • notifyAll()会唤醒所有wait线程

协程

image.png
  • 协程是一种基于线程之上,但又比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行),具有对内核来说不可见的特性。这样带来的好处就是性能得到了很大的提升
  • 问题: 协程的特点在于是一个线程执行,那和多线程比,协程有何优势
    • 线程的切换由操作系统调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率
    • 线程是默认stack大小是1M,而协程更轻量,接近1k.因此可以在相同的内存中开启更多的协程
    • 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多
  • 问题: Java中是否存在协程
    • kilim quasar框架
  • 注意
    • 协程适用于被阻塞的,且需要大量并发的场景(网络io)
    • 不适合大量计算的场景

猜你喜欢

转载自juejin.im/post/7100994816468582407