Shawn's Blog

蒸汽兔

Java与线程

字数:352 字 阅读时长:约 1 分钟 阅读

前言

我们都知道,线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源调度(内存地址、文件I/O等),又可以独立调度。

线程模型

主流的操作系统都提供了线程实现,Jav语言则是提供了在不同硬件和操作系统平台下对线程操作的统一处理,

每个已经调用过start()方法且还未结束的java.lang.Thread类的实例就代表这一个线程。

其实Thread类与大部分的Java类库API有着显著差别,它的所有关键方法都被声明为Native。在Java中,一个Native方法往往就意味着这个方法没有使用或无法使用平台无关的手段来实现(通常最高效率的手段就是平台相关的手段)。 那么线程的实现其实是有三种方式的:

  • 使用内核线程实现(1:1实现);
  • 使用用户线程实现(1:N)实现;
  • 使用用户线程加轻量级进程混合实现;

2.1 内核线程实现 (1:1)

使用内核线程实现的方式被称为1:1实现。

内核线程(Kernel Levvel Thread,KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,内核通过操纵调度器 Scheduler 对线程进行调度,并负责将线程的任务映射到各个处理器上。 其实程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口—— 轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常所讲的线程。这种轻量级进程与内存线程之间1:1的关系称为一对一的线程模型

IMG_3840

局限性

  1. 由于是基于内核线程实现的,所以各种线程操作(创建、析构及同步),都需要进行系统调用,系统调用就要在用户态和内核态中来回切换。
  2. 每个轻量级进程都需要一个内核线程的支持,因此需要消耗一定的内核资源,所以一个系统支持轻量级进程的数量是有限的

2.2 用户线程实现 (1:N)

使用用户线程实现的方式被称为1:N实现

广义上来讲,一个线程只要不是内核线程,都可以任务是用户线程(User Threa,UT)的一种。从定义上来看轻量级进程不是内核线程也就是属于用户线程,但是它始终是建立在内核之上的,所以效率会受到限制,并不具备用户线程的优点。

用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核帮助。如果程序实现得当,不需要切换内核态,因此操作可以是非常快且低消耗的,也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。 这种进程与用户线程之间1:N的关系称为一对多的线程模型

IMG_3842

优势

  1. 不需要系统内核支援, 用户线程的建立、同步、销毁和调度完全在用户态中完成

劣势

  1. 同样也是没有内核支援,所有的线程操作都需要由用户程序自己去处理,这样就会导致线程的一些问题处理起来就很困难,甚至有些是不可能实现的。
  2. “阻塞如何处理”、”多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来异常困难,甚至不可能实现。

Java、Ruby等语言都曾经使用过用户线程,最终又都放弃了使用它。但是近年新的以高并发为卖点的编程语言又普遍支持了用户线程,比如Golang、Erlang。

2.3 混合实现 (N:M)

线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式,被称为N:M实现

用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。

IMG_3841

2.4 Java线程的实现

Java线程如何实现并不受Java虚拟机规范约束,这是一个与具体虚拟机相关的画图。

Java线程在早期的Classic虚拟机上(Jdk1.2以前),是基于一种被称为“绿色线程”(Green Threads)的用户线程实现的,但从Jdk1.3起,“主流”平台上的“主流”商用Java虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现,即采用1:1的线程模型。

操作系统支持怎样的线程模型,在很大程度想会影响上面的Java虚拟机的线程是怎么样映射的,这一点咋不同的平台上很难达成一致,因此《Java虚拟机规范》中才不去限定Java线程需要使用哪种线程模型来实现。

线程模型只对线程的并发规模和操作成本产生影响,对Java程序的编码和运行过程来说,这些差异都是完全透明的

Java 线程调度

线程调度是指系统为线程分配处理使用权的过程,调度主要方式有两种,分别是协同式(Cooperative Threads-Scheduling)线程调度抢占式(Preemptive Threads-Scheduling)线程调度

3.1 协同式线程调度 (Cooperative Threads-Scheduling)

线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上去。

优点

实现简单,切换操作对线程自己是可知的,所以一般没有什么线程同步问题。

缺点

线程执行时间不可控制,甚至如果一个线程的代码编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。

3.2 抢占式线程调度 (Preemptive Threads-Scheduling)

每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。

优点

可以主动让出执行时间(例如Java的Thread::yield()方法),并且线程的执行时间是系统可控的,也不会有一个线程导致整个系统阻塞的问题。

缺点

无法主动获取执行时间。

3.3 结尾

Java使用的就是抢占式线程调度,虽然这种方式的线程调度是系统自己的完成的,但是我们可以给操作系统一些建议,就是通过设置线程优先级来实现。Java语言一共设置了10个级别的线程优先级。在两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。

Java 线程生命周期

4.1 线程状态

Java语言定义了6种线程状态,在任意一个时间点钟,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间切换。

/**
 * ref java.lang.Thread
 * A thread state.  A thread can be in one of the following states:
 **/
public enum State {
    NEW,

    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;
}
  • 新建(New):创建后尚未启动的线程处于这种状态。
  • 运行(Runnable):包括操作系统线程状态中的Running和Ready,也就是处理此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。
  • 无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显示唤醒。 以下方法会让线程陷入无限期等待状态: 1、没有设置Timeout参数的Object::wait()方法; 2、没有设置Timeout参数的Thread::join()方法; 3LockSupport::park()方法。
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。 以下方法会让线程进入限期等待状态: 1Thread::sleep()方法; 2、设置了Timeout参数的Object::wait()方法; 3、设置了Timeout参数的Thread::join()方法; 4LockSupport::parkNanos()方法; 5LockSupport::parkUntil()方法;
  • 阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间 ,或者唤醒动作发生。在程序进入同步区域的时候,线程将进入这种状态。
  • 结束(Terminated):已终止线程的线程状态,线程已经结束执行。

这6种状态在遇到特定事件发生的时候将会互相转换,他们的转换关系如下图:

线程状态转换关系

参考资料


© Shawn Jim. All rights reserved. 本站总访问量 次, 访客数 人次.