多线程实现原理
相关阅读
彻底搞懂 CPU 中的内存结构
Java 内存模型 ,一篇就够了!
首先,多线程的出现是为了加快处理任务的效率,结合之前说过的底层 CPU 的介绍我们可以知道,在操作系统层面上,线程是操作系统任务调度的最小单位,进程是资源分配的最小单位,一个进程可以包含多个线程,线程共享进程中的资源。
说个形象的比喻,进程就像是一个巨大的工厂在作业,而线程就是工厂中的一个个的生产线。我下文中常说的任务,指的是和线程对应的一个个的生产线。
类比到我们的 Java 项目中来,可能有些任务我们可以抽离出来,独立执行,而这就可以引入线程的概念,又因为万物皆是对象,于是 Java 中就定义了 Thread 对象来表示线程。
但是前面说过了,线程的出现是为了执行任务的,那 Java 中又怎么表示具体的任务的呢?Java 中提供了 Runnable 和 Callable 接口,具体的任务逻辑自己实现。
说到这你有没有感觉,我们常说的 Java 中实现多线程的方式有点奇怪,说实在的 Java 中线程的实现就一个啊,就是继承 Thread 类,其它的都是与线程绑定的任务呢。但是,理解就行,面试的时候慎重说只有一种。
在 Java 语言层面,我们是很轻松,只需继承 Thread 类,调用 start 方法就算搞定,但是在 JVM 和操作系统层面又是如何实现的呢?
JVM 实现多线程使用的编程语言是 C++ ,具体的实现还是依赖与不同的操作系统,更多的工作还是在处理线程安全,这个我们上次已经说过了,在 JMM 中的设计中可以看出。
重头戏来了,操作系统如何实现多线程,主要有 3 种方式,使用内核线程实现,使用用户线程实现和使用用户线程加轻量级进程混合实现。
使用内核线程实现
内核线程(KLT,Kernel-Level Thread),直接由操作系统内核(Kernel,即内核)支持的线程。由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核叫做多线程内核。
程序一般不会去直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(LWP),即通常意义上的线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。轻量级进程与内核线程之间 1:1 关系称为一对一的线程模型。
优点:在多处理器系统,内核能够同时调度同一进程中的多个线程并发执行;如果一个进程的某个线程被阻塞,内核可以调度该进程的其他线程运行,也可以调度其他进程的线程运行;线程的切换比较快,切换开销小;内核本身也可以采用多线程技术,可以提高系统的执行速度和效率;
缺点:对于用户线程的调度,其模式切换开销比较大,在同一个进程中,从一个线程切换到另一个线程时,需要从用户态转到核心态实现,这是因为,线程运行在用户态,而线程调度和管理是在内核实现的,因此系统开销较大;
使用用户线程实现
用户线程(User Thread)是在用户空间实现的,对线程的创建、撤销、通信、同步等功能的实现,无需内核的支持,因而内核完全不知道用户级线程的存在;此时进程和用户线程是一对多的关系。
线程切换不需要转到内核空间。对一个进程而言,其所有的线程管理数据结构均在该进程自己的用户空间,管理线程切换的线程库也在用户地址空间运行,从而不需要切换到内核方式来管理线程,从而节省了内核态和用户态之间切换的开销;
又因为对线程的创建、撤销、通信、同步等功能的实现不需要内核来完成,都需要用户程序自己来处理,用户程序一般都比较复杂,所以,用这种方式来处理创建多线程的程序越来越少。
使用用户线程加轻量级进程混合实现
在这种模式下,即存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换等操作依然廉价,并且可以支持大规模的用户线程并发。
而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及时处理映射,并且用户线程的系统要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程和轻量级进程的数量比是不确定的,即为 N:M 的关系。
上面 3 种创建进程的方式是不同的操作系统可能会采用的,而 Java 中具体采用哪一种要看 JVM 的具体实现。
Java 线程在 JDK 1.2 之前,是基于称为“绿色线程”(Green Threads)的用户线程实现的,而在 JDK 1.2 中,线程模型替换为基于操作系统原生线程模型来实现。
因此,在目前的 JDK 版本中,操作系统支持怎样的线程模型,在很大程度上决定了 Java 虚拟机的线程是怎样映射的,这点在不同的平台上没有办法达成一致,虚拟机规范中也并未限定 Java 线程需要使用哪种线程模型来实现。
对于 Sun JDK 来说,它的 Windows 版与 Linux 版都是使用一对一的线程模型实现的,一条 Java 线程就映射到一条轻量级进程之中,因为 Windows 和 Linux 系统提供的线程模型就是一对一的。
而在 Solaris 平台中,由于操作系统的线程特性可以同时支持一对一及多对多的线程模型,因此在 Solaris 版的 JDK 中也对应提供了两个平台专有的虚拟机参数:-XX:+UseLWPSynchronization(默认值)和-XX:+UseBoundThreads 来明确指定虚拟机使用哪种线程模型。