输出倒逼输入系列之 阻塞 or 等待?
背景
最近在学习 《趣谈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 定义的线程状态 与 底层 操作系统 还是有区别的
如图所示,类比 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 实现原理:
如图所示:
Lock 运用巧妙的数据结构 + park 挂起线程操作,达到线程同步的效果,与 Synchronized 的阻塞核心概念完全不同
总结
通过几个案例简单解释了 阻塞 和等待区别与联系,希望大家对线程在不同场景的状态会更了解,更清楚,bingo!