Skip to content

Latest commit

 

History

History
97 lines (51 loc) · 5.71 KB

2019-01-31-Java线程是如何实现的?.md

File metadata and controls

97 lines (51 loc) · 5.71 KB
title date author img categories tags summary
Java线程是如何实现的?
2019-01-31 02:13:59 -0800
Cowboy
后端
并发编程
通过本节,你将会从底层的角度知道为什么建议不要显示创建线程而要使用线程池;为什么synchronized是重量级锁。

什么是线程

通过本节,你将会从底层的角度知道为什么建议不要显示创建线程而要使用线程池;为什么synchronized是重量级锁。

多任务并行调度系统

随着科技的发展,硬件条件虽然不断提高,但并没有像摩尔定律那样成倍增长,因此现代处理器的重心逐渐由高频转向多核心。由此操作系统(OS)自然也要做出相应的更改,原来可能是一个OS内核对应一个CPU,用户将任务提交给内核后由内核来调度CPU来完成计算:

图1

现在CPU有了多核心之后就转变成了如下架构(以我的华硕i5-4核心CPU为例):

图2

内核线程(KLT,Kernel-level Thread)就是内核的一个分身,这是为了支持多任务并行调度。

并行和并发是两个完全不同的概念:

图1中一个OS内核对应一个CPU,同一时刻多个用户请求提交给OS内核,这一个OS内核通过调度CPU在这些任务间来回切换执行,但同一时刻有且只有一个任务享受到CPU执行权,这是并发。

图2中,一个OS内核多个分身,同一时刻多个用户请求提交给OS内核,OS内核通过将任务分发给它的分身,由它的分身再去调度CPU的不同内核,这时同一时刻也许任务1跑在CPU0上,而任务2跑在CPU3上,但仍然可能存在任务10在等待调度,这是并行(多个任务可能同时在执行,也可能有部分再等待调度)。

Java线程模型

在OS支持多任务并行调度系统后,自然对外提供了相应的JNI接口(Java Native Interface),以使开发者的应用程序(App)能够使用,那么Java应用程序中的Thread(用户线程)和OS中的内核线程之间是什么关系呢?

内核线程是由OS直接支持的线程,这种线程由内核完成上下文切换,内核通过调度器(Scheduler)将线程的任务映射到各个CPU(或CPU核心)上,每个内核线程可以看做内核的一个分身。应用程序一般不会直接使用内核线程,而是使用OS提供的一种高级接口:轻量级进程(LWP,Light-weight Process),也就是我们通常所说的线程。

于是就有了以下三种Java线程的实现方案:

1、基于LWP

既然OS为我们提供了LWP接口,那么我们将线程的实现都交给LWP不就完了?

image

使用这种方式,Java线程的实现确实是简单了(所有功能委托给LWP),但是LWP是对KLT的一层封装,也就是说这样的话Java线程将直接依赖于内核线程,应用程序的所有线程操作都会调用OS来帮我们完成(这将引起一次次的用户态到内核态的转换,开销是比较大的)。

2、不依托OS,自己实现

既然OS提供的接口开销这么大,那我不用你的,我自己实现一个应用程序进程内调度的线程可不可以呢?

image

也就是说我自己实现我应用程序的线程(UT,User Thread),不调用你(OS)的LWP,这样你就不知道我UT的存在了(我应用程序的UT)不用你管了。

如此的话,用户态切换到内核态的开销确实是没了(线程的调度由应用程序自己实现了,并没有依靠LWP),但是这样也屏蔽掉了OS为应用程序提供的便利(强大的应用程序通常都会充分利用OS提供的便利)。而且不依托于OS自己实现的线程很难管理(诸如什么时候该让他执行,什么时候该让他挂起,这都是很棘手的事情)。

3、合作才是共赢

基于以上两种方案存在的缺陷,我们能否将两者形成互补之势呢?即既能够将线程的管理交给LWP,又能利用用户态线程(UT)的操作开销小于内核态的KLT的特点,取百家之长呢?

通过以上思考,Java线程模型诞生了:

image

如图所示,Java线程还是使用用户线程库的线程实现,这样有两个好处:

  • 大部分操作不需要调用OS,自己就能完成
  • 用户线程库的线程数不像KLT有严格的限制,基本上够应用程序使用

而LWP还是要用的,不过他只负责线程管理的部分(如线程的创建、销毁、调度)。

这种设计的巧妙之处就是利用OS提供的LWP作为用户态和内核态的桥梁,应用程序自己能够解决的部分就尽量在用户态完成,而确实需要切换到内核态的操作再委托LWP调用OS。

通过本节,我们知道:线程的创建、销毁、调度都会引起用户态到内核态的切换,开销较大。

因此你可能会发现阿里巴巴开发规约会要求你不要显示的创建线程,而是使用线程池,这样的话不会频繁的创建、销毁线程。

还有就是synchronized为什么会被称为重量级锁,这时因为当获取monitor的线程发现此monitor已被其他线程持有时会陷入BLOCKED阻塞状态,而这一操作是通过LWP来完成的,会引起用户态到内核态的切换,这甚至会导致切换到内核态调用OS将线程阻塞的时间比同步代码块执行所需的时间还要长。