JAVA与线程

线程的实现方式

概述

在传统的操作系统中,拥有资源和独立调度的基本单位都是进程。在引入线程的操作系统中,线程是独立调度的基本单位,进程是资源拥有的基本单位。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换

根据操作系统内核是否对线程可感知,可以把线程分为内核线程和用户线程

名称 描述
用户级线程(User-LevelThread, ULT) 由应用程序所支持的线程实现, 内核意识不到用户级线程的实现
内核级线程(Kemel-LevelThread, KLT) 内核级线程又称为内核支持的线程

实现线程主要有3种方式:使用内核线程、使用用户级线程和使用用户线程加轻量级进程混合实现。

使用内核线程实现

内核线程就是直接由操作系统内核支持的线程,这种线程有内核来完成线程切换,内核通过调度器(Scheduler) 对线程进行调度,并负责将线程的任务映射到各个处理器上。

轻量级进程(LWP)

程序一般不会直接使用内核线程,而是去使用内核线程的一种高级接口———轻量级进程(LWP),轻量级进程就是我们通常意义上所讲的线程,轻量级进程(LWP)是一种由内核支持的用户线程。它是基于内核线程的高级抽象,因此只有先支持内核线程,才能有LWP。

轻量级进程与内核线程之间的1:1的关系

优点

  • 多处理器系统中,内核能够并行执行同一进程内的多个线程
  • 如果进程中的一个线程被阻塞,能够切换同一进程内的其他线程继续执行(用户级线程的一个缺点)
  • 所有能够阻塞线程的调用都以系统调用的形式实现,代价可观
  • 当一个线程阻塞时,内核根据选择可以运行另一个进程的线程,而用户空间实现的线程中,运行时系统始终运行自己进程中的线程
  • 信号是发给进程而不是线程的,当一个信号到达时,应该由哪一个线程处理它?线程可以“注册”它们感兴趣的信号

缺点

对于用户的线程切换而言,其模式切换到饿开销较大,在同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到核心态进行,这是因为用户的线程在用户态运行,而线程调度和管理实在内核中实现的,系统开销较大。

使用用户线程实现

LWP虽然本质上属于用户线程,但LWP线程库是建立在内核之上的,LWP的许多操作都要进行系统调用,因此效率不高。而这里的用户线程指的是完全建立在用户空间的线程库,用户线程的建立,同步,销毁,调度完全在用户空间完成,不需要内核的帮助。因此这种线程的操作是极其快速的且低消耗的。

进程与用户线程之间的1:N的关系

从中可以看出,进程中包含线程,用户线程在用户空间中实现,内核并没有直接对用户线程进程调度,内核的调度对象和传统进程一样,还是进程本身,内核并不知道用户线程的存在。用户线程之间的调度由在用户空间实现的线程库实现。

优点

  • 可以在不支持线程的操作系统中实现。
  • 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多, 因为保存线程状态的过程和调用程序都只是本地过程
  • 允许每个进程定制自己的调度算法,线程管理比较灵活。这就是必须自己写管理程序,与内核线程的区别
  • 线程能够利用的表空间和堆栈空间比内核级线程多
  • 不需要陷阱,不需要上下文切换,也不需要对内存高速缓存进行刷新,使得线程调用非常快捷
  • 线程的调度不需要内核直接参与,控制简单。

缺点

  • 线程发生I/O或页面故障引起的阻塞时,如果调用阻塞系统调用则内核由于不知道有多线程的存在,而会阻塞整个进程从而阻塞所有线程, 因此同一进程中只能同时有一个线程在运行
  • 页面失效也会产生类似的问题。
  • 一个单独的进程内部,没有时钟中断,所以不可能用轮转调度的方式调度线程
  • 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用

补充
在用户级线程中,每个进程里的线程表由运行时系统管理。当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重新启动该线程所需的信息,与内核在进程表中存放的进程的信息完全一样

使用用户线程加轻量级进程混合实现

这种模型对应着恐龙书中多对多模型。用户线程库还是完全建立在用户空间中,因此用户线程的操作还是很廉价,因此可以建立任意多需要的用户线程。操作系统提供了 LWP 作为用户线程和内核线程之间的桥梁。 LWP 还是和前面提到的一样,具有内核线程支持,是内核的调度单元,并且用户线程的系统调用要通过 LWP ,因此进程中某个用户线程的阻塞不会影响整个进程的执行。用户线程库将建立的用户线程关联到 LWP 上, LWP 与用户线程的数量不一定一致。当内核调度到某个 LWP 上时,此时与该 LWP 关联的用户线程就被执行。

用户线程与轻量级进程之间的N:M的关系

JAVA线程调度

进程调度属于低级调度,用来决定就绪队列中的那个进程获取处理器,然后有分派程序将执行把处理器分派给该进程的具体操作。进程调度的方式有两个非抢占式调度和抢占式调度

非抢占方式(Non-preemptive Mode)

概念: 当某一进程正在处理机上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,该进程仍继续执行,直到其完成或发生某种事件而进入完成或阻 塞状态时,才把处理机分配给更为重要或紧迫的进程

引起进程调度的因素:

  • 正在执行的进程执行完毕, 或因发生某事件而不能再继续执行
  • 执行中的进程因提出I/O请求而暂停执行;
  • 在进程通信或同步过程中执行了某种原语操作,如wait、Block、Wakeup原语

优点: 算法简单,系统开销小

缺点: 紧急任务不能及时响应;短进程到达要等待长进程运行结束
适用于大多数批处理系统环境

抢占方式(Preemptive Mode)

概念: 当某一进程正在处理机上执行时,若有某个更为重要或紧迫的进程进入就绪队列,则立即暂停正在执行的进程,将处理机分配给这个更为重要或紧迫的进程

抢占式调度主要有以下原则

  • 优先权原则 允许高优先权的新到进程抢占当前进程的处理机
  • 短作业(进程)优先原则允许执行时间短的新到进程抢占当前进程的处理机
  • 时间片原则 时间片用完后停止执行,重新进行调度,适用于分时系统

优点: 适于时间要求严格的实时系统

缺点: 调度算法复杂,系统开销大

状态转换

线程间的状态转换:

  1. 新建(new):新创建了一个线程对象。

  2. 可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。

  3. 运行(running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

  4. 阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

    • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。
    • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
    • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
  5. 死亡(dead):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

参考

0%