线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。JAVA语言中每个已经执行了 start() 且还未结束的 java.lang.Thread 类的实例就代表了一个线程。Thread 类的所有关键方法都是声明为Native的,在Java API中,一个Native方法往往意味着这个方法没有使用或者无法使用平台无关的手段来实现(也可能为了执行效率)。
实现线程主要有3种方法:内核线程、用户线程、用户线程加轻量级进程混合实现。
1.1 使用内核线程实现
内核线程(Kernel-Level Thread, KLT)就是由操作系统内核支持的线程,内核通过操作调度器(Scheduler)对线程进行调度。
程序一般不会直接使用内核线程,而是去使用内核线程的一种高级接口-轻量级进程(Light Weight Process, LWP),轻量级进程就是我们通常意义上所讲的线程。
这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型,如下图。
优点: 每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统中阻塞了,也不会影响整个进程继续工作。缺点: 由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用,需要在用户态和内核态中来回切换,代价相对较高。
1.2 使用用户线程实现
用户线程(User Thread, UT)指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。
用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。因此操作比内核线程更快速,并可以支持更大的线程数量。
这种进程(不是轻量级进程)与用户线程之间1:N的关系称为一对多的线程模型。
优点: 线程所有操作都在用户态完成,不需要切换到内核态,如果实现得当,那么速度非常快,资源消耗也很低,可以支持更大规模的线程数量。
缺点: 由于线程的操作没有操作系统内核的支援,所有线程操作都需要用户程序自己实现,创建、线程间的切换、调度都需要用户程序处理,并且由于操作系统只把处理器资源分配到了进程,除了线程切换获取处理器外,线程阻塞如何处理,或者把进程内的线程映射到其他处理器,都是非常困难的。
现在使用用户线程的程序越来越少了,像Java、Ruby等语言都曾经使用过用户线程,不过现在都放弃了。
1.3 使用用户线程加轻量级进程混合实现
在这种混合实现下,既存在用户线程,也存在轻量级进程。
用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价。而操作系统提供轻量级进程则作为用户线程和内核线程之间的桥梁。这样可以使用内核提供的线程调度功能及处理器映射,用户线程调用通过轻量级线程来完成。
用户线程与轻量级进程的数量比是不定的,即为N:M的关系,这种就是多对多的线程模型。
Java 线程在JDK 1.2之前,是基于用户线程实现的,JDK 1.2中替换为基于操作系统原生线程模型来实现。目前的JDK版本中,操作系统支持怎样的线程模型,很大程序上决定了Java虚拟机的线程是怎样映射的。
虚拟机规范中并未限定Java线程需要使用哪种线程模型来实现。
对于Sun JDK来说,它的windows版与linux版都是使用一对一的线程模型实现的,一条Java线程映射到一条轻量级进程中,即内核线程的实现方式。
线程调度是指系统为线程分配处理器使用权的过程。主要有协同式线程调度和抢占式线程调度两种。
如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。如果一个线程写得有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。
如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
Java 使用的线程调度方法就是抢占式调度。