面试官:Java 线程可以无限创建吗?
1. 面试连环 call
Java 线程可以无限创建吗?
Java 线程和操作系统线程有什么关联?
操作系统为什么要区分内核态和用户态?
⏩要想解答这些问题,我们要先从操作系统线程开始说起,让我们开始吧🎉🎉🎉
2. 操作系统线程
2.1 内核态和用户态
根据进程访问资源的特点,我们可以把进程在系统上的运行分为两个级别:
用户态(User Mode) : 用户态运行的进程可以直接读取用户程序的数据,拥有较低的权限。
内核态(Kernel Mode):内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序等,不受限制,拥有非常高的权限。当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。
![](https://static001.geekbang.org/infoq/ac/acd7c8641f03b9cf0aed80edae8c82f1.jpeg?x-oss-process=image%2Fresize%2Cp_80%2Fauto-orient%2C1)
那为什么要区分用户态和内核态呢?
在 CPU 的所有指令中,有一些指令是比较危险的比如内存分配、设置时钟、IO 处理等,如果所有的程序都能使用这些指令的话,会对系统的正常运行造成灾难性地影响。因此,我们需要限制这些危险指令只能内核态运行。这些只能由操作系统内核态执行的指令也被叫做 特权指令 。
如果计算机系统中只有一个内核态,那么所有程序或进程都必须共享系统资源,例如内存、CPU、硬盘等,这将导致系统资源的竞争和冲突,从而影响系统性能和效率。并且,这样也会让系统的安全性降低,毕竟所有程序或进程都具有相同的特权级别和访问权限。
2.2 用户态线程
在早期的操作系统中,所有的线程都是在用户态下实现,操作系统只能调度线程所属的进程,而无法调度线程。
在这种模型下,用户需要自己定义线程的数据结构、创建、销毁、调度和维护等,这些线程运行在某个进程内,操作系统直接对进程进行调度。
![](https://static001.geekbang.org/infoq/e3/e3f64d0e985f6cb336476283826d6f56.png)
『优点』:
即使操作系统原生不支持线程,我们也可以通过库函数来支持线程
线程的调度只发生在用户态,避免了操作系统从内核态到用户态的转换开销。
『缺点』:
由于操作系统无法调度线程,CPU 的时间片切换是以进程为维度的,如果进程中某个线程进行了耗时比较长的操作,那么由于用户态中没有时钟中断机制,就会导致此进程中的其它线程因为得不到 CPU 资源而长时间的持续等待;
如果某个线程进行系统调用时比如缺页中断而导致了线程阻塞,此时操作系统也会阻塞整个进程,即使这个进程中其它线程还在工作。
2.3 内核态线程
现代操作系统,包括 Windows、Linux、Mac OS X 和 Solaris 等,都支持内核线程。线程运行在内核空间,直接由内核负责,由内核来完成调度。
此时我们可以直接使用操作系统中已经内置好的线程,线程的创建、销毁、调度和维护等,都直接由操作系统的内核来实现,我们只需要使用系统调用就好了,不需要像用户级线程那样自己设计线程调度。
![](https://static001.geekbang.org/infoq/dd/dda8fbf40ea508fd0aa06c5b2ede3695.png)
内核线程和用户线程的对应关系并不完全是 1 对 1,其关联模式有三种
2.4 线程模型
多对一线程模型:
多个用户线程对应到同一个内核线程上,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。这样,极大地减少了创建内核态线程的成本,但是线程不可以并行。因此,这种模型现在基本上用的很少。
![](https://static001.geekbang.org/infoq/bb/bb67dfe5926bec2fc6f9a05e8b4ad26b.png)
『优点』:
用户线程的很多操作对内核来说都是透明的,不需要用户态和内核态的频繁切换。使线程的创建、调度、同步等非常快。
『缺点』:
由于多个用户线程对应到同一个内核线程,如果其中一个用户线程阻塞,那么该其他用户线程也无法执行。
内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的优先级调度等操作
一对一线程模型:
该模型为每个用户态的线程分配一个单独的内核态线程,在这种情况下,每个用户态都需要通过系统调用创建一个绑定的内核线程。 这种模型允许所有线程并行执行,能够充分利用多核优势。目前 Linux 中的线程、OpenJDK Java 线程等采用的都是一对一线程模型。每一个 JVM 线程,都有一个对应的内核线程。
![](https://static001.geekbang.org/infoq/d3/d37f8bab27f894a8df4314263bd42b92.png)
『优点』:
解决了多对一模型的阻塞和调度问题
实现起来较为简单
『缺点』:
每创建一个用户线程,相应地就需要创建一个内核线程,开销较大,因此需要限制整个系统的线程数量。
对用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换。
多对多线程模型:
这种模式下会为 n 个用户态线程分配 m 个内核态线程。m 通常小于 n。一种可行的策略是将 m 设置为核数。这种多对多的关系,减少了内核线程,同时也保证了多核心并行。多对多模型中线程的调度需要由内核态和用户态一起来实现,例如线程间同步需要用户态和内核态共同实现。用户态和内核态的分工合作导致实现该模型非常复杂。
PS: Linux 多线程模型曾经也想使用该模型,但它太复杂,要对内核进行大范围改动,所以还是采用了一对一的模型!
![](https://static001.geekbang.org/infoq/f7/f7f8f33362fa580eb0d18d0494b4092b.png)
『优点』:
多对多模型将任意数量的用户线程复用到相同或更少数量的内核线程上,结合了一对一和多对一模型的最佳特性
用户对创建的线程数没有限制
『缺点』:
实现起来非常复杂
3. Java 线程
3.1 线程库
在进入 Java 线程主题之前,有必要讲解一下线程库 Thread library 的概念。
线程库就是为开发人员提供创建和管理线程的一套 API。线程库不仅可以在用户空间中实现,还可以在内核空间中实现。前者涉及仅在用户空间内实现的 API 函数,没有内核支持。后者涉及系统调用,也就是说调用库中的一个 API 函数将会导致对内核的系统调用,并且需要具有线程库支持的内核。
下面简单介绍下三个主要的线程库:
POSIX 线程:是[POSIX]的[线程]标准,定义了创建和操纵线程的一套[API]。实现 POSIX 线程标准的库常被称作 pthreads,一般用于[Unix-like] POSIX 系统,如[Linux]、 [Solaris]。
Win32 线程:用于 Window 操作系统的内核级线程库
Java 线程:Java 线程 API 通常采用宿主系统的线程库来实现,也就是说在 Win 系统上,Java 线程 API 通常采用 Win API 来实现,在 UNIX 类系统上,采用 Pthread 来实现。
3.1 Java 线程模型
在 JDK 1.2 之前,Java 线程是基于称为 "绿色线程"(Green Threads)的用户级线程实现的,JVM 开发了自己的一套线程库或者说线程管理机制。
在 JDK 1.2 及以后,JVM 选择了更加稳定且方便使用的操作系统原生的内核级线程,通过系统调用,将线程的调度交给了操作系统内核。而对于不同的操作系统来说,它们本身的设计思路基本上是完全不一样的,因此它们各自对于线程的设计也存在种种差异,所以 JVM 中明确声明了:虚拟机中的线程状态,不反应任何操作系统中的线程状态。
因此,现今 Java 中线程的本质,其实就是操作系统中的线程,其线程库和线程模型很大程度上依赖于操作系统(宿主系统)的具体实现,比如在 Windows 中 Java 就是基于 Wind32 线程库来管理线程,且 Windows 采用的是一对一的线程模型。
3.2 Java 线程创建数量
每个线程都有一个线程栈空间通过-Xss 设置,可以通过 JVM 配置,JVM 的默认栈大小
![](https://static001.geekbang.org/infoq/98/98336fcb39d6e19f526776aecb198e90.png)
不考虑系统限制,可以通过如下公式计算,得出最大线程数量
线程数量=(机器本身可用内存-JVM 分配的堆内存)/Xss 的值
根据计算公式,得出如下结论:
结论 1:JVM 堆越大,系统创建的线程数量越小。
结论 2:当-Xss 的值越小,可生成线程数量越多。
假如我们的容器内存大小是 8G,堆大小是 4096M,走-Xss 默认值,可以得出 最大线程数量:4096 个。
我们知道操作系统分配给每个进程的内存大小是有限制的,比如 32 位的 Windows 是 2G。因此操作系统对一个进程下的线程数量是有限制的,不能无限的增多。
如果考虑系统限制,主要跟以下几个参数有关系
/proc/sys/kernel/pid_max 增大,线程数量增大,pid_max 有最高值,超过之后不再改变,而且 32,64 位也不一样
/proc/sys/kernel/thread-max 系统可以生成最大线程数量
线程是非常宝贵的资源,我们要严格控制线程的数量
文章转载自:程序员世杰
评论