阿烫的后花园

烫烫烫烫烫烫

线程的实现

  实现线程主要有 3 种方式:使用内核线程实现、使用用户线程实现、使用用户线程加轻量级进程混合实现。

1.使用内核线程实现

  内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多个任务,支持多线程的内核叫做多线程内核

  程序一般不会直接去使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),也就是我们通常意义上所讲的线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程和内核线程之间 1 : 1 的关系称为一对一的线程模型,如下图。

image.png

  由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态和内核态中来回切换,其次,每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此,一个系统支持的轻量级进程的数量是有限的。

  Sun JDK 的 windows 版与 Linux 版都是使用此模型实现的。

2.使用用户线程实现

  广义上讲,一个线程只要不是内核线程,就可以认为是用户线程(User Thread,UT),那轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,效率会受到限制。

  狭义上的用户线程是指完全建立在用户控件的线程库上,系统内核不能感知线程存在,用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。这种线程是不需要切换到内核态,因此操作时非常快速且低消耗的。这种实现可以支持规模更大的线程数量,这种进程与用户线程之间 1 : N 的关系称为一对多的线程模型。

image.png

  使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理,创建、切换、调度、销毁都非常复杂。现在使用蔗渣模型的程序越来越少了,Ruby、Java曾经使用过用户线程,最终又放弃了它。

3.使用用户线程加轻量级进程混合实现

  许多Unix操作系统,如 Solaris、HO-UX 提供了 N : M 的线程模型实现,这种模型其实就是将上面两种方式混合,如下图:
image.png

Java线程调度

  线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种:协同式线程调度抢占式线程调度。

  协同式调度的多线程系统中,线程的执行时间是由线程本身来控制的,线程把自己的工作执行完了后,要主动通知系统切换到另外一个线程上。这种实现的最大好处是实现简单,切换操作对于线程自己事可知的,所以没有线程同步的问题。坏处也很明显:线程执行时间不可控,甚至如果一个线程编写有问题,一直不告诉系统进行线程切换,那程序就会阻塞,甚至系统崩溃。

  如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。Java使用的就是抢占式调度

操作系统中的线(进)程状态转换

  首先要简单说下操作系统中线程的状态,其有五个状态,除了创建和终止,还有就绪、运行和阻塞状态,当发生 I/O 或其它事件时,会从运行状态转变为阻塞状态,当 I/O 结束时,便从阻塞状态转变为就绪状态,此时只要有 CPU 时间片就可以立即变为运行状态。
进程转换状态

不难发现操作系统中的线程状态都是围绕着 CPU 来说的

Java线程状态转换

  Java线程定义了 6 种线程状态:

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

  需要注意的是,Java中的线程状态并不是与操作系统中的线程状态一一对应的。查看 Thread 类的源代码,可以看见 RUNNABLE 状态的注释中说了处于这个状态线程可能正在 JVM 中执行,也可能正在等待操作系统的某种资源。所以,RUNNABLE 状态至少包括了操作系统中的就绪和运行状态的。

1
2
3
4
5
6
7
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE

  当发生 I/O 阻塞时,操作系统概念下的线程会切换到阻塞状态,但 Java 的线程状态也有阻塞,还有无期限等待和期限等待,那此时 Java 的线程状态是什么呢?其实也还是 RUNNABLE,可以写一个简单 demo 来验证:

image.png

  同理,网络阻塞时,Java的线程状态也还是 RUNNABLE。Java 中的 RUNNABLE 实际对应了操作系统中的 就绪状态、运行状态和部分阻塞状态。看起来很混乱,那 Java 为什么要这么做呢?前面 RUNNABLE 的注释说到该状态的线程可能正在JVM中执行,也可能正在等操作系统的资源,JVM 把很多东西都看作了资源,硬盘、CPU、网卡也罢,只要有一个东西在为线程服务,那就可以理解为这个线程是在“执行”的,虽然 I/O 阻塞, CPU 不执行了,但网卡还在监听干活啊,只是暂时没有数据罢了。操作系统的线程状态都是围绕着 CPU 来说的,这与 JVM 的侧重点不同。

  如果 Java 中的线程处于期限等待、无期限等待或阻塞状态,那说明这个线程真的是一点事都不干了,而且是你自己不让它干的,例如调用了 sleep()、wait() 方法。

参考:https://my.oschina.net/goldenshaw/blog/705397

主内存与工作内存

  Java内存模型规定了所有变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。三者的关系如下图。

image.png

阅读全文 »

  在部分的商用虚拟机中, Java 程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或者代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的效率,在运行时,虚拟机会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler, JIT 编译器)

阅读全文 »

运行时栈帧结构

  栈帧是虚拟机用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。每一个方法从调入开始到执行完成的过程,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

  在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的 Code 属性中,因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现,

阅读全文 »

  文章参考虚拟机规范Chapter 4. The class File Format,这篇文章只说常量池相关部分。常量池中主要存放两大类常量:字面量(Literal)和 符号引用(Symbolic References)。字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Full Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
阅读全文 »

类加载过程

  类从被加载到虚拟机内存开始,到卸载出内存位置,它的整个生命周期包括以下 7 个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)其中验证、准备、解析 3个部分统称为链接(Linking)

图1 类的生命周期

阅读全文 »

  Java中,short 、byte、char 类型的数据在做运算的时候,都会默认提升为 int,如下面的代码,需要将等于号右边的强制转为 short 才可以通过编译。

1
2
3
4
5
6
7
public static void main(String[] args) {
short a = 1;
short b = 2;
a = a + b; // 编译不过
short c = a + b; // 编译不过
short d = (short) (a+b); // 编译通过
}

  为什么两个 short 相加会变成 int,有的解释说,两个 short 相加可能溢出,所以用 int 来接就不会溢出,那这样的话,两个 int 相加岂不应该是 long 类型吗?其实本质的原因要从字节码开始讲起。

本文出现了一些字节码指令,如果想详细查看,请参考Java虚拟机规范 Chapter 6. The Java Virtual Machine Instruction Set

阅读全文 »

垃圾回收算法需要考虑三件事情:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

先奉上 Java平台标准版HotSpot虚拟机垃圾收集调优指南

对象已死?

  哪些对象需要回收,就是那些死了的对象,即不可能再被任何途径所使用的对象。有两种主流的方法可以判断对象是否需要回收,引用计数法可达性分析法

阅读全文 »

  链路层的主体是在网络适配器中实现的,有时也称为网络接口卡。位于网络适配器核心的是链路层控制器,该控制器通常是一个实现了许多链路层服务(成帧、链路接入、差错检测等)的专用芯片,因此链路层的许多功能是由硬件实现的。

  有两种类型的网络链路:点对点链路和广播链路。点对点链路由链路一段的单个发送方和链路另一端的单个接收方组成,如点对点协议(point-to-point protocol,PPP)就是这样的协议。广播链路能够让多个发送和接收结点都连接到相同的、单一的、共享的广播信道上。当任何一个结点传输一个帧时,信道广播该帧,每个其它结点都收到一个副本。广播信道通常用于局域网中。

阅读全文 »

转发和路由选择

  在网络中的每一台主机和路由器中都有一个网络层部分。网络层为了将分组从一台发送主机移动到另外一台接收主机,需要两个重要功能:

  • 转发:当一个分组到达路由器的一条输入链路时,路由器必须将该分组移动到适当的输出链路。
  • 路由选择:当分组从发送方流向接收方时,网络层必须决定这些分组所采取的路由或路径。计算这些路径的算法被称为路由选择算法

    转发是指将分组从一个输入链路接口转移到适当的输出链路接口的路由器本地动作。路由选择是指网络范围的过程,以决定分组从源到目的地所采取的的端对端路径,是一种宏观上的策略。

  每台路由器都有一张转发表。路由器通过检查到达分组首部字段的值来转发分组,然后使用该值在转发表索引查询该分组的输出链路接口。

阅读全文 »
0%