热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java并发编程解析|关于线程机制的那些事,你究竟了解多少?

Java并发编程解析|关于线程机制的那些事,你究竟了解多少?-苍穹之边,浩瀚之挚,眰恦之美;悟心悟性,善始善终,惟善惟道!——朝槿《朝槿兮年说》写在开头众所周知,在计算机操作系统中
苍穹之边,浩瀚之挚,眰恦之美; 悟心悟性,善始善终,惟善惟道! —— 朝槿《朝槿兮年说》

写在开头

众所周知,在计算机操作系统中,进程(Process)是一个很关键的概念,最本质的理解就是操作系统执行的一个应用程序(Application Program)。与每个进程相关的是地址空间(Address Space)。其中,描述的是从某个最小值的存储位置(通常是0)到最大值的存储位置的列表。在这个地址空间中,进程可以进行读写操作。地址空间中可以存放可执行程序,以及程序需要的数据和栈针。与每个进程相关的资源集合。通常包括寄存器(Registers),打开的文件清单,突发的系统报警,有关的进程清单和其他执行程序的信息。其中,寄存器主要包括程序计数器(Program Counter)和堆栈指针(Stack Pointer)。从一定程度上,我们可以把进程当作容纳运行一个程序所有信息的一个容器(Container)。

操作系统中可以使用进程来描述一个程序的执行过程,进程拥有该程序的所有数据(包括一些I/O分配情况、内存分配情况等),也就是该程序的一个载体,所以进程有一个特点就是资源分配的单位,这一点十分重要。进程还有一个特点就是调度执行,交替执行以提高资源利用率。

操作系统管理进程(创建、切换进程、分配与回收等)开销是很大的,比如进程创建时还需要创建PCB,分配内存独立的内空间,建立映射表,创建资源,进程切换时还需要切换资源,如切换对应的内存映射表,进程退出时还需要释放资源。

由此不难得出一个结论,每一个进程都有一个地址空间(Address Space)和一个控制线程(Control Thead)。但是,操作系统有了进程为何要出现线程呢?主要是因为,虽然进程任然是资源分配的单位,但是调度执行却交给了线程,因为线程是在进程的内部,线程间的切换不用切换资源,不用切换映射表,只需要简单的在进程内部切换一下PC指针和保存一些寄存器即可,这也就更轻量了(避免不了不同进程间的线程切换)。

基本概述

线程(Thread)既保留了并发执行的优点,也避免了进程切换的代价。

假设现在有一个网络服务器,此时没有线程的概念,该服务器程序用到多个进程,如用一个进程监听客户端的请求,当客户端连接上后就分派出(复制出一个子进程)一个进程给该用户(每个进程都有独立的资源),用于监听该用户发送的数据并处理(即多进程程序),此时设想一下,这多个进程切来切去,每次切换的时候都需要切换资源,是不是很耗费资源。

此时,引入了线程之后,网络服务器这个程序是一个进程,进程用于承载该程序的资源,首先用进程中的一个线程监听请求,每次连接客户端都分配一个线程给用户(多线程程序),此时处理器只需要在这几个线程中切换即可,线程的切换不需要切换资源(进程时资源级的切换,线程是指令级的切换),那么多个线程只需要共享进程中的资源即可,其运行速度和执行效率也得到了提升。

由此可见,操作系统引入线程后,调度和分派是在线程上完成的,但是某些活动会影响进程中的所有线程,因此这些活动必须在进程级对他们进行管理。如挂起操作会挂起所有线程,因为所有线程共享进程的用户地址空间。引入了线程最关键的体现在以下两个方面:

  • 线程的创建、终止和调度更轻量
  • 线程间的通信不进过内核,不需要用户态->内核态的转换

但是,同时也增加了程序的开发难度,如果开发者对于线程机制的掌握和认识不够准确,也会陷入技术困惑。

线程模型

所有线程共享进程的状态和资源,所以线程都驻留在同一块地址空间中,并可访问相同的数据。

对于有线程和无线程的区别,其中主要是体现在用户栈和内核栈两个关键:

  • 用户栈用于保存用户进程的子程序间相互调用的参数、返回值以及局部变量等信息(保存普通方法的栈)
  • 内核栈是程序发生系统调用时内核态调用方法时的栈;用户地址空间则是进程的程序和数据存放的空间,线程是没有自己的用户地址空间的

一般来说,用户栈和内核栈已经在线程中独有,也证明了线程成为了任务调度的基本单位,这些线程都共享进程所持有的资源,线程控制块中存放了寄存器的值、优先级、线程状态等信息。

在操作系统层面,线程也有“生老病死”,专业的说法叫有生命周期。虽然不同的开发语言对于操作系统线程进行了不同的封装,但是对于线程的生命周期这部分,基本上是相通的。每一个线程基本都有如下特征:

  1. 类似进程,线程也有执行状态(生命周期),因为线程也是一个执行过程
  2. 线程的上下文,线程切换时也需要进行保护现场
  3. 执行栈,保存系统调用时的一些参数和中间结果
  4. 少量的,线程私有的局部变量的存储空间,不再拥有大量的存储空间
  5. 与进程内其他线程共享的内存和资源的访问
  6. 线程控制块TCB,存放上下文切换的信息,同PCB

可以看出,对于有生命周期的事物,要学好和掌握它,思路非常简单,只要能搞懂生命周期中各个节点的状态转换机制即可。

线程分类

线程分为用户级线程(User-Level Thread,ULT)和内核级线程(Kernel-Level Thread,KLT),内核级线程又叫做轻量级进程(Light-Weight Process,LWP)。

用户级线程(User-Level Thread,ULT)

在纯ULT软件中,管理线程的所有工作都是应用程序完成,内核意识不到线程的存在,线程完全是由线程库提供的,创建、销毁、调度线程、线程间传递消息等,还包括保存上下文都是由它管控的,如果可以的话我们自己也可以实现自己的线程库,只要合理的组织线程即可。

但是用户级线程所有的活动都发生在用户空间和一个进程中,系统感知不到用户级线程的存在,所以系统依旧是以进程的方式调度。

当线程1发生系统调用等阻塞了,此时系统就会认为该进程阻塞了,操作系统会把CPU时间片分配给其他进程,在此期间,根据线程库维护的数据结构来看,线程1任然处于运行状态,但在处理器执行的角度,线程2是不处于运行状态的,也分不到时间片。

这也导致了用户级线程一旦阻塞,就会阻塞进程中的所有线程,使得其他线程也得不到运行。使用用户级线程(ULT)如下特点:

优点:

  • 所有线程的管理都在一个进程的用户空间中,线程的切换不需要内核模式特权,不需要系统调用,从而节省了用户态到内核态转换的开销
  • 线程的调度更灵活,可以为每个不同的应用程序量身定制更合适的调度算法,因为这些调度算法都可以自己实现,不需要更改操作系统底层的调度程序
  • ULT可以在任何操作系统下运行,即便是不支持线程的操作系统也能实现,线程库是供所有应用程序共享的一组应用级函数

缺点:

  • 在执行一个系统调用时不仅仅是阻塞当前线程,还会阻塞进程中的所有线程
  • ULT不能利用多处理技术,操作系统看不到线程,所以内核一次只能把一个进程分配给一个处理器,因此一个进程中的所有线程不能够并行执行,只能够并发执行,相当于一个进程内实现了多道程序设计

    解决这两个问题的方法有:

  • 把应用程序写出多进程程序,但是该方法消除了线程的主要优点
  • 套管技术:把产生阻塞的系统调用转化为一个非阻塞的系统调用

综上所述,用户级线程(User-Level Thread,ULT)适合计算密集型的,因为不需要IO操作 ,不会阻塞整个进程。

内核级线程(Kernel-Level Thread,KLT)

在KLT软件中,管理线程的所有工作均由内核完成,应用级没有线程管理代码,只有一个到内核线程的API。

内核为进程级进程内的所有线程维护上下文信息,调度由内核基于线程完成。

该方法克服了ULT的两个缺点。首先,内核可以把一个进程中的线程分配个多个处理器中;其次,进程中的某个线程阻塞了,内核还可以调度同一个进程中的其他线程。

缺点是:在把控制权从一个线程传送到另一个进程的线程时,需要切换到内核模式,开销较大。

综上所述,KLT并发性更好,适合I/O操作较多的程序。

混合线程(Hybrid-Approach Thread,HAT)

有些操作系统提供了ULT和KLT的混合体:线程创建完全在用户空间中完成,线程的调度和同步也在应用程序中进行,一个应用程序中的多个用户级线程会被映射到一些(小于等于用户级线数)内核级线程上,进程和线程的比为 N:M,N<=M,ULT中比值为1:N,KLT为1:1

同一个应用程序中的多个线程可在多个处理器上并行的运行,某个引起阻塞的系统调用不会阻塞整个进程。

综上所述,内核级线程(KLT)和用户级线程(User-Level Thread,ULT)对比分析如下:

线程生命周期

一个线程的生命周期基本上可以这个“五态模型”来描述,主要分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。其中:

  1. 初始状态,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
  2. 可运行状态,指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
  3. 运行状态:当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。
  4. 休眠状态:运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
  5. 终止状态:线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束。
版权声明:本文为博主原创文章,遵循相关版权协议,如若转载或者分享请附上原文出处链接和链接来源。

推荐阅读
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 本文介绍了Java的集合及其实现类,包括数据结构、抽象类和具体实现类的关系,详细介绍了List接口及其实现类ArrayList的基本操作和特点。文章通过提供相关参考文档和链接,帮助读者更好地理解和使用Java的集合类。 ... [详细]
  • 标题: ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 集合的遍历方式及其局限性
    本文介绍了Java中集合的遍历方式,重点介绍了for-each语句的用法和优势。同时指出了for-each语句无法引用数组或集合的索引的局限性。通过示例代码展示了for-each语句的使用方法,并提供了改写为for语句版本的方法。 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • Java 11相对于Java 8,OptaPlanner性能提升有多大?
    本文通过基准测试比较了Java 11和Java 8对OptaPlanner的性能提升。测试结果表明,在相同的硬件环境下,Java 11相对于Java 8在垃圾回收方面表现更好,从而提升了OptaPlanner的性能。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
author-avatar
冬天的芦苇2011
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有