当前位置: 首页 > 图灵资讯 > 技术篇> Java中线程介绍

Java中线程介绍

来源:图灵教育
时间:2024-03-10 16:21:54
一、基础知识 线程和过程概念 进程
  • 程序由指令和数据组成,但是指令运行,数据读写,必须将指令加载到 CPU,将数据加载到内存中。磁盘、网络等设备也需要在指令操作过程中使用。该过程用于加载指令、管理内存、管理内存 IO 的 。
  • 当一个程序从磁盘加载程序的代码到内存运行时,就开始了一个过程。
  • 这个过程可以看作是程序的一个例子。大多数程序可以同时运行多个例子过程(如记事本、绘图、浏览器等) 等等),有些程序只是

一个例子过程可以启动(如网易云音乐,360 安全卫士等。).

  • 操作系统以流程为单位,分配系统资源(CPU时间片、内存等资源),流程是资源分配的最小单位。
线程
  • 在这个过程中,线程是一个实体,一个过程可以有多个线程,一个线程必须有一个父进程。
  • 线程是指令流,指令流中的指令按一定顺序交给 CPU 执行 。
  • 线程有时被称为轻量级过程(Lightweight Process,LWP),是实施操作系统调度(CPU调度)的最小单位。
过程与线程的区别
  • 过程基本上是独立的,线程存在于过程中,是过程的子集
  • 该过程拥有共享的内存空间等内部线程共享资源
  • 通信在过程中比较复杂
    • 同一台计算机的过程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的过程通信需要通过网络遵守共同协议,如 HTTP
  • 由于共享过程中的内存,例如,线程通信相对简单;多个线程可以访问相同的共享变量
  • 线程较轻,线程上下文切换成本一般低于过程上下文切换
进程间通信的方式
  1. 管道(pipe)及有名管道(named pipe):管道可用于父子之间有亲缘关系的通信。除了管道的功能外,著名的管道还允许无亲缘关系的通信。
  2. 信号(signal):信号是软件层面中断机制的模拟,是一种复杂的通信方式,用于通知过程中发生的事件。可以说,一个过程收到的信号与处理器收到的中断请求是一致的。
  3. 消息队列(message queue):消息队列(MQ)克服了上述两种通信方式中信号量有限的缺点,可以按照一定的规则将新信息添加到信息队列中,并可以从信息队列中读取信息。
  4. 共享内存(shared memory):可以说,这是进程间最有用的通信方式。它允许多个过程访问相同的内存空间,不同的过程可以及时看到共享内存中的数据更新。这种方法需要依赖于一些同步操作,如相互排斥锁和信号量。
  5. 信号量(semaphore):它主要作为同步和相同过程的不同线程之间的相互排斥手段。
  6. 套接字(socket):进程间通信机制更为普遍,可用于网络中不同机器之间的进程间通信,应用广泛。
同步和相互排斥线程

线程同步

它是指线程之间的限制关系。一个线程的执行取决于另一个线程的信息。当你没有得到另一个线程的信息时,你应该等待,直到信息到达时才被唤醒。(例如,阻塞队列。当队列为空时,等待add信号,否则take将被阻塞)

线程互斥

它是指在单个线程访问中共享的过程系统资源的排他性。当一个共享资源必须使用多个线程时,最多只允许一个线程在任何时候使用,其他使用该资源的线程必须等待,直到占用资源的人释放该资源。线程相互排斥可视为一种特殊的线程同步。

线程同步互斥的控制方法

  • 临界区(相互排斥):通过串行访问公共资源或多线程代码,速度快,适合控制数据访问。(一段时间内只允许一个线程访问的资源称为临界资源)。
  • 相互排斥(相互排斥):为协调共享资源的单独访问而设计的。
  • 信号量(互斥):设计是为了控制有限数量的用户资源。
  • 事件(同步):通知线程中发生了一些事件,从而开始后续任务。
上下文切换(Context switch)

是指CPU从一个过程或线程切换到另一个过程或线程。

寄存器是CPU内部快速内存的一小部分(与CPU外部慢RAM主内存相比)。它通过快速访问常用值来加速计算机程序的执行。

程序计数器是一种特殊的寄存器,指示CPU在其指令序列中的位置,并保存正在执行的指令地址或下一个指令地址

CPU上的过程(包括线程)可以更详细地描述为内核(即操作系统的核心)执行以下活动:

  1. 暂停一个过程的处理,并将CPU状态(即上下文)存储在内存中的某个地方
  2. 从内存中获取下一个过程的上下文(主内存加载到工作内存,间接可见性),并在CPU的寄存器中恢复
  3. 返回到程序计数器指示的位置(即返回到中断过程的代码行)以恢复过程。

上下文切换只能发生在核心模式下。核心模式是CPU的特权模式,只有核心运行才有权访问所有内存位置和其他系统资源。其他程序(包括应用程序)最初在用户模式下运行,但它们可以通过系统调用部分核心代码。

线程模型

KLT模型,ULT模型。

JVM使用的KLT模型,Java线程和OS线程保持1:1的映射关系(一个java线程对应一个操作系统的线程),具体使用线程(创建、销毁、销毁)调度)由操作系统管理。

ULT模型:线程操作由自己控制,实现起来比较复杂。

Kernel Mode

在核心模式下,执行代码可以完全无限地访问底部硬件。它可以执行任何CPU指令和引用任何内存地址。核心模式通常保留操作系统的最低级别和最值得信赖的功能。核心模式下的崩溃是灾难性的;它们会使整个计算机瘫痪。

User Mode

在用户模式下,执行代码不能直接访问硬件或引用内存。在用户模式下运行的代码必须委托给系统api访问硬件或内存。由于这种隔离提供的保护,用户模式下的崩溃总是可以恢复的。在您的计算机上运行的大部分代码将在用户模式下执行。

切换到内核模式的几种情况:
  1. 系统调用。
  2. 异常事件。当事先发生一些不可知的异常时,会切换到内核状态,以执行相关的异常事件。
  3. 设备中断。在使用外围设备时,如果外围设备完成用户请求,将向CPU发送中断信号。此时,CPU将暂停执行下一个原始指令,并转移到处理中断事件。此时,如果它是在用户状态中,它自然会切换到核心状态
CPU保护模式

x86 CPU提供四个保护环(protection rings):0、1、2.3.通常只使用0环(内核)和3环(用户)

Java线程状态:新建(new)、可执行(runnable)、执行(running)、阻塞(blocked)、结束(terminated)

线程生命周期 操作系统5线程状态
  1. 新建:指线程已创建,但不允许分配 CPU 执行。这种状态是编程语言独有的,但这里所谓的创建只是在编程语言层面,而在操作系统层面,线程尚未创建。例如:new Thread()
  2. 就绪:指线程可以分配 CPU 执行。在这种状态下,已经成功地创建了真正的操作系统线程,因此可以分配 CPU 执行。例如:调用start方法,线程进入可执行状态,等待cpu时间片。
  3. 执行:有空闲的 CPU 当操作系统将其分配到可操作状态的线程 CPU 线程状态转换为运行状态。线程获得cup时间片并开始执行。如果时间片丢失,则进入就绪状态.(例如:手动调用yield放弃cpu执行权)
  4. 阻塞:执行中的线程,等待一些资源(锁,IO、或手动执行slep)【获取资源后,可进入就绪状态\执行态】
  5. 结束:线程的最终状态。意味着线程的生命周期结束。(线程正常结束,异常结束(如中断)、手动调用stop、机器宕机(crash))

结束状态不能转换为任何状态,即线程结束后不能重新调整。(线程结束后,同一对象不能调用start方法)

JAVA6种线程状态

NEW、RUNNABLE、WAITING、TIMED_WAITING、BLOCKED、TERMINATED

协程

目的

追求最大限度地发挥硬件性能,提高软件速度。

原理

将当前任务挂在某一点,保存堆栈信息,执行另一个任务,然后恢复原堆栈信息并继续执行(整个过程不需要上下文切换)

Java原生不支持协程,需要在纯Java代码中使用协程,需要引入第三方包。例如:quasar

协程在线程上运行,当一个协程完成后,可以选择主动让出,让另一个协程在当前线程上运行。

协程并没有增加线程的数量,而是在线程的基础上分时重用多个协程,协程的切换在用户状态下完成,切换成本远低于线程从用户状态到核心状态的成本。

golang语言自然支持协程

二、Java线程 Java线程实现方法

Runnable、Thread、Callable是线程的实现

本质上,Java中只有一种方法可以实现线程 Thread()创建线程,调用Thread#start启动线程,最终调用Thread#run方法

Java线程调度机制

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

Java线程是抢占式调度

若要控制线程执行优先级,可通过设置线程优先级来完成。

Java语言有10个级别的线程优先级(Thread.MIN__PRIORITYThread.MAX_PRIORITY),当两个线程同时处于ready状态时,系统越容易选择执行优先级越高的线程。但优先级不是很可靠,因为Java线程是通过映射到系统的原始线程来实现的,所以线程调度最终取决于操作系统。

协同线程调度

线程执行时间由线程本身控制,线程执行完自己的工作后,应主动通知系统切换到另一个线程。

优点:实现简单,切换操作对线程本身是可知的,没有线程同步问题。

缺点:线程执行时间无法控制。如果一个线程有问题,它可能会被阻塞(导致饥饿)。

抢占线程调度

执行时间由系统分配,线程切换不由线程本身决定(Java中,Thread.yield()可以放弃执行时间,但无法获得执行时间)。线程执行时间系统可控,整个过程不会被阻塞。