线程 进程 协程 管程

线程

最小的执行单元,也叫轻量级进程

一个进程可以有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)和系统分配的资源(CPU,I/O,打开的文件)

但是各个线程拥有自己的栈空间。

多线程

更多的CPU核心 现代计算机处理器性能的提升方式,已经从追求更高的主频向追求更多的核心发展,所以处理器的核心数量会越来越多,充分地利用处理器的核心则会显著地提高程序的性能。而程序使用多线程技术,就可以将计算逻辑分配到多个处理器核心上,显著减少程序的处理时间,从而随着更多处理器核心的加入而变得更有效率。

更快的响应时间 我们经常要针对复杂的业务编写出复杂的代码,如果使用多线程技术,就可以将数据一致性不强的操作派发给其他线程处理(也可以是消息队列),如上传图片、发送邮件、生成订单等。这样响应用户请求的线程就能够尽快地完成处理,大大地缩短了响应时间,从而提升了用户体验。

更好的编程模型 Java为多线程编程提供了良好且一致的编程模型,使开发人员能够更加专注于问题的解决,开发者只需为此问题建立合适的业务模型,而无需绞尽脑汁地考虑如何实现多线程。一旦开发人员建立好了业务模型,稍作修改就可以将其方便地映射到Java提供的多线程编程模型上。

进程

进程是操作系统进行资源分和调度的一个基本单位。每个进程都有自己独立的内存空间,不同进程通过进程之间通信来通信。进程上下文切换开销比较大,但相对稳定安全

区别:进程是系统进行资源的分配(如地址和文件等)的基本单位;线程是CPU调度和分派的基本单位

进程是拥有资源的一个独立单位,线程不拥有系统资源(但线程有自己的堆栈和局部变量)但可以访问隶属于进程的资源。进程所维护的是程序包含的资源(静态资源),如:地址空间,打开的文件句柄集,文件系统状态,信号量处理handler等;线程维护的是线程运行的相关资源,如运行栈等

一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程

切换:上下文切换包含了寄存器的存储和程序计数器存储的指令内容。进程切换与线程切换的最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中线程进行线程切换时,不涉及虚拟地址空间的交换

协程Coroutines

协程是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序管理的轻量级线程也被称为用户空间线程,对于内核而言是不可见的。正如同进程中存在多条线程一样,线程中也可以存在多个协程。

协程在运行时也有自己的寄存器、上下文和栈,协程的调度完全由用户控制,协程调度切换时,会将寄存器上下文和栈保存到分配的私有内存区域中,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

纤程

纤程(Fiber)是Microsoft组织为了帮助企业程序的更好移植到Windows系统,而在操做系统中增加的一个概念,由操作系统内核根据对应的调度算法进行控制,也是一种轻量级的线程。

纤程和协程的概念一致,都是线程的多对一模型,但有些地方会区分开来,但从协程的本质概念上来谈:纤程、绿色线程、微线程这些概念都属于协程的范围。纤程和协程的区别在于:

  • 纤程是OS级别的实现,而协程是语言级别的实现,纤程被OS内核控制,协程对于内核而言不可见。

管程Monitors

管程(Monitors)提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。

管程就是代表共享资源的数据结构以及对该数据结构实时操作的一组过程组成的资源管理程序共同构成的一个操作系统资源管理模块

比如一次只允许一个进程访问的资源或者多个进程只能互斥访问的资源,该访问需要同步操作,比如信号量就是一种方便有效的进程同步机制。但信号量的方式要求每个访问临界资源的进程都具有wait和signal操作。这样使大量的同步操作分散在各个进程中,不仅给系统管理带来了麻烦,而且会因同步操作的使用不当导致死锁。管程就是为了解决这样的问题而产生的。

管程定义了一个数据结构和能喂并发进程锁执行(在该数据结构上的一组操作),这组操作能够同步进程和改变管程中的数据

操作系统中管理的各种软件和硬件资源,均可用数据结构抽象地描述其资源特性,即用少量信息和对该资源所执行的操作来表征该资源,而忽略它们的内部结构和实现细节。利用共享数据结构抽象地表示系统中的共享资源。而把对该共享数据结构实施的操作定义为一组过程,如资源的请求和释放过程request和release。进程对共享资源的申请、释放和其他操作,都是通过这组过程对共享数据结构的操作来实现的,这组过程还可以根据资源的情况接受或阻塞进程的访问,确保每次仅有一个进程使用该共享资源,这样就可以统一管理对共享资源的所有访问,实现临界资源互斥访问。
管程就是这样被请求和释放临界资源的进程所调用。

java的内置管程synchronized

  • Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。
  • MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。

java中每一个对象都是一个monitor(java中翻译为资源监视器),java中提供的synchronzied关键字wait(),notify(),notifyAll()方法,都是monitor的一部分,或者说在JDK1.5页就是JUC没有出现前,java都是通过Monitor来实现并发的

Monitor在OS中或者别处的翻译是管程,我也更倾向于翻译为管程,Java中使用的是MESA管程,本文呢,我们就来模拟实现霍尔管和汉森管程,来加强我们对并发编程的能力和增加对线程同步问题的理解。

在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。

Hasen 模型、Hoare 模型和 MESA 模型的一个核心区别就是当条件满足后,如何通知相关线程。管程要求同一时刻只允许一个线程执行,那当线程 T2 的操作使线程 T1 等待的条件满足时,T1 和 T2 究竟谁可以执行呢?

Hasen 模型里面,要求 notify() 放在代码的最后,这样 T2 通知完 T1 后,T2 就结束了,然后 T1 再执行,这样就能保证同一时刻只有一个线程执行。

Hoare 模型里面,T2 通知完 T1 后,T2 阻塞,T1 马上执行;等 T1 执行完,再唤醒 T2,也能保证同一时刻只有一个线程执行。但是相比 Hasen 模型,T2 多了一次阻塞唤醒操作。

MESA 管程里面,T2 通知完 T1 后,T2 还是会接着执行,T1 并不立即执行,仅仅是从条件变量的等待队列进到入口等待队列里面。这样做的好处是 notify() 不用放到代码的最后,T2 也没有多余的阻塞唤醒操作。但是也有个副作用,就是当 T1 再次执行的时候,可能曾经满足的条件,现在已经不满足了,所以需要以循环方式检验条件变量。

活锁和饥饿锁

活锁

活锁值得是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直尝试--失败--尝试--是啊比的过程。处于活锁的实体是在不断改变状态的。活锁有可能自行解开

如果事务T1封锁了数据R,事务T2又请求封锁R,于是T2等待。T3也请求封锁R,当T1释放了R上的封锁后,系统首先批准了T3的请求,T2仍然等待。然后T4又请求封锁R,当T3释放了R上的封锁之后,系统又批准了T4的请求......T2可能永远等待。
活锁应该是一系列进程在轮询地等待某个不可能为真的条件为真。活锁的时候进程是不会blocked,这会导致耗尽CPU资源

线程智力不够,且都秉承着“谦让”的原则。(优先级低)主动将资源释放给他人使用,那么出现资源不断在两个线程中跳动,而没有一个线程可以同时拿到所有资源而正常执行

饥饿锁

锁饥饿是指一条长时间等待的线程无法获取到锁资源或执行所需的资源,而后面来的新线程反而“插队”先获取了资源执行,最终导致这条长时间等待的线程出现饥饿。

ReetrantLock的非公平锁就有可能导致线程饥饿的情况出现,因为线程到来的先后顺序无法决定锁的获取,可能第二条到来的线程在第十八条线程获取锁成功后,它也不一定能够成功获取锁。

锁饥饿这种问题可以采用公平锁的方式解决,这样可以确保线程获取锁的顺序是按照请求锁的先后顺序进行的。但实际开发过程中,从性能角度而言,非公平锁的性能会远远超出公平锁,非公平锁的吞吐量会比公平锁更高。

当然,如果你使用了多线程编程,但是在分配纤程组时没有合理的设置线程优先级,导致高优先级的线程一直吞噬低优先级的资源,导致低优先级的线程一直无法获取到资源执行,最终也会使低优先级的线程产生饥饿。

Last modification:January 6, 2023
如果觉得我的文章对你有用,请随意赞赏