likes
comments
collection
share

输出倒逼输入系列之 阻塞 or 等待?

作者站长头像
站长
· 阅读数 24

背景

最近在学习 《趣谈Linux 操作系统》这个专栏,为了能让自己学的更深刻,就想把期间学到的知识与自己熟知的事物进行关联,利用强制输出进行倒逼深刻输入

学习期间,发现一个词语为“阻塞”的状态好像和认知里不一样,因为自己先学过java线程里的状态,它里面的阻塞状态为线程抢夺资源没成功且还在盘桓的时候的状态,而在操作系统里,阻塞其实可以理解为 睡眠状态 或 挂起状态

那么,我们来详细的解剖一下

详解

操作系统线程状态

《趣谈Linux 操作系统》第12章节,讲到 进程的数据结构,其中涉及到任务状态,这里简单概述一下

首先在操作系统中,不管是进程还是线程,都统一是 任务的概念,它的数据结构 为 task_strcut。

任务在执行期间,会伴随着好几个状态,在数据结构中,用 一个 long state 变量 表示,且 state 是通过 bitset 的方式设置的,一位表示一个状态

大概的状态有 :

1 Task_Running

2 Task_Interruptible

3 Task_Uninterruptible

4 Task_killable

5 Task_Stopped

6 Exit

  其中 2 ,3,4 都为 睡眠状态,区别在于对于信号的响应能力不一样,可被信号中断,不可被信号中断

从上述状态可知,底层操作系统可大致分为三个状态阶段 

运行 -- > 挂起(运行一半,歇一会儿) --> 结束  

JVM 中线程的状态

JVM 使用 特定的数据结构 来管理线程状态,每个 线程对象都包含一个状态字段

JVM 定义的线程状态 与 底层 操作系统 还是有区别的

输出倒逼输入系列之 阻塞 or 等待?

如图所示,类比 java 中的线程状态分为:

1 运行状态

2 阻塞状态

3 等待状态

4 超时等待状态

5 终止状态

阻塞状态

其中 2 阻塞状态,一般是指 使用了 Synchrnized 关键字。它的底层实现原理为,在操作系统的共享内存临界区加锁保护,一次只能一个线程进入。阻塞时,线程不一定进入睡眠状态,可能一直在自旋,或者被提交给操作系统挂起,这里又涉及 Synchronized 轻量级锁 和 重量级锁的概念

这里我觉得可以通俗的翻译为,抢夺资源中

这里贴一段 synchronized 底层实现逻辑:

每个 Java 对象都有一个 关联的 Monitor 对象,也称为 监视器锁 和 内置锁,这个 monitor 对象在 JVM中管理着对象的同步和互斥访问

当一个线程进入 一个 synchronized 代码块或方法时,它会尝试获取该对象的 Monitor对象锁,这个Monitor对象锁可以被视为共享资源,在底层操作系统中,Mutex 提供了共享资源互斥访问的同步机制

等待状态

3 waiting 状态,就是提交给操作系统,挂起或睡眠的操作

真正的挂起,用得比较多的是,LockSupport 工具 提供的 park() 方法

底层不同操作系统提供的方法不一样,但都是使线程 进入睡眠状态

对应的唤醒方法为 unpark() ,用于 唤醒沉睡的线程

  java 并发编程里常见的线程暂停一会儿的方法有:

sleep()

wait()

await()等

都是 进入睡眠状态,等待被唤醒或中断

 

这里详细解说下 sleep() 和 wait():

sleep 是 直接调用操作系统提供的方法,进行挂起

而 wait 是由 JVM 的内部机制实现的线程的等待和唤醒,即进入某个对象的Monitor 对象的等待队列,然后间接的调用操作系统的 挂起方法,这样可以避免线程忙等待

 

其他状态

4 超时等待状态 ,就是定时唤醒线程,结束睡眠状态 即可

 

点题

操作系统内核没有阻塞的概念,它的阻塞意思为“挂起”,“睡眠”状态

  在JVM 层面,对于线程的状态,多了一个比较显眼的状态,为 BLOCKED,这依赖于底层的 Mutex 锁等同步机制,来保护临界区代码的互斥访问。  

JVM 在 操作系统的 底层实现上,自定义加工了 自己的状态,更满足日常使用

重点,区分JVM中 阻塞 和 等待状态的区别,阻塞其实研究到底,是也有可能会被挂起的,他最终也会演变成等待状态

 

有趣的现象

阻塞状态 是线程阻塞在 进入 Synchronized 关键字修饰的方法或代码块是的状态,但是阻塞在 java concurrent 包中 Lock接口的线程状态却是等待状态,因为Lock它对于阻塞的实现 均使用了 Locksupport 类中的 park 方法,它的阻塞实现,其实就是 等待状态

再聊聊 Lock

之前Synchronized 关键字,涉及到底层操作系统调用,显得复杂笨重,不好控制。

Lock 出现的背景,就是希望在代码设计层面,维护一个简单的共享资源,多个线程围绕着这个资源,进行抢夺且更新资源状态,再配合 使用LockSupport 类中的方法,按需挂起线程。

这样可以更灵活和扩展性更大  

AQS(抽象队列同步器) 是这个设计的核心要点

它的内部维护一个一个 同步状态 state 和一个 同步队列

同步状态表示共享资源的状态,同步队列是一个双向链表,用来存储抢夺资源且未抢到的线程

 

简单介绍一下AQS 实现原理:

如图所示:

输出倒逼输入系列之 阻塞 or 等待?

   

Lock 运用巧妙的数据结构 + park 挂起线程操作,达到线程同步的效果,与 Synchronized 的阻塞核心概念完全不同

 

总结

通过几个案例简单解释了 阻塞 和等待区别与联系,希望大家对线程在不同场景的状态会更了解,更清楚,bingo!