💡超详细 | 如何唤醒被阻塞的 socket 线程?线程阻塞原理、线程池、fork/vfork彻底讲明白!
- 一、什么是阻塞?为什么线程会阻塞?
- 二、socket线程被阻塞的典型场景
- 🧠 解法思路:
- 三、线程的几种阻塞状态和唤醒方式一览
- 四、如何判断线程是繁忙还是阻塞?
- 五、就绪状态线程在等待什么?
- 六、如何实现线程池?
- 🏗 线程池原理:
- 💡 Java 中线程池核心类:
- 七、fork 和 vfork 的区别?
- 🧠 补充:写时复制(COW)
- 八、进阶:写时复制(COW)原理
- 九、Server 阻塞状态示意
- 🔚 总结
一、什么是阻塞?为什么线程会阻塞?
线程阻塞是一种等待某个事件发生的状态。比如等待 I/O 完成、锁释放、条件满足、子线程结束等。
常见导致线程阻塞的情况有:
阻塞方式 | 常见场景 | 唤醒方式 |
---|---|---|
Thread.sleep(ms) | 让出 CPU 一段时间 | 时间到了自动唤醒 |
Object.wait() | 等待被 notify() | 被 notify() / notifyAll() 唤醒 |
Thread.join() | 主线程等待子线程结束 | 子线程执行完毕自动唤醒 |
LockSupport.park() | 显式挂起线程 | 调用 unpark(Thread) 唤醒 |
socket accept() / read() | 阻塞等待客户端连接/数据 | 客户端连接/发送数据 |
synchronized 锁竞争 | 等待锁资源 | 锁释放后参与竞争 |
二、socket线程被阻塞的典型场景
举个最常见的 ServerSocket 场景:
ServerSocket server = new ServerSocket(8888);
Socket client = server.accept(); // 这里会阻塞直到有客户端连接
当执行 accept()
时,线程会阻塞等待客户端连接,直到有连接进入,线程才会被唤醒继续执行。
📌 问题:如果我想手动唤醒这个被阻塞的 accept()
怎么办?
🧠 解法思路:
- 关闭 socket:关闭 ServerSocket,会抛出异常从
accept()
中跳出。 - 使用 selector/epoll 实现非阻塞,比如
NIO
。 - 新建连接触发 accept() 返回:可以在本地建立一个 loopback 连接触发
accept()
返回。
// 触发唤醒 server.accept()
Socket socket = new Socket("localhost", 8888);
三、线程的几种阻塞状态和唤醒方式一览
阻塞方式 | 描述 | 唤醒方式 | 示例 |
---|---|---|---|
Thread.sleep() | 睡眠,释放 CPU,不释放锁 | 时间到 | Thread.sleep(1000) |
Object.wait() | 等待 notify,释放锁 | notify() /notifyAll() | obj.wait() |
Thread.join() | 等待其他线程结束 | 子线程结束 | t.join() |
Thread.suspend() (已弃用) | 挂起线程 | resume() | 慎用 |
LockSupport.park() | 显式挂起 | unpark() | 常用于 AQS |
socket.accept() | 阻塞等待连接 | 有连接到来 / 被关闭 |
四、如何判断线程是繁忙还是阻塞?
- Linux 可用
ps -e -o pid,state,cmd
查看线程状态:
状态码 | 描述 |
---|---|
R | Running(可运行) |
S | Sleeping(可中断阻塞) |
D | Uninterruptible sleep(不可中断阻塞) |
Z | Zombie |
T | Stopped(暂停) |
👀 繁忙线程处于 Running 状态
😴 阻塞线程常见为 Sleeping 或 D 状态
五、就绪状态线程在等待什么?
就绪(Runnable)状态的线程,已经满足了运行条件,但等待操作系统调度器为它分配 CPU 才能执行。它处在一个“抢票”的阶段。
六、如何实现线程池?
线程池是一种线程复用机制,用于提高系统并发能力和资源利用率。基本思路如下:
🏗 线程池原理:
- 初始化 N 个工作线程,进入等待状态
- 维护一个任务队列(生产者-消费者模型)
- 有任务时唤醒线程执行,执行完毕后重新等待
- 没任务时线程阻塞等待
💡 Java 中线程池核心类:
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> doTask());
自己实现线程池的基本结构如下:
// 工作线程不断从任务队列中拉取任务
while (true) {Runnable task = taskQueue.take(); // 阻塞等待任务task.run();
}
七、fork 和 vfork 的区别?
特性 | fork() | vfork() |
---|---|---|
共享地址空间 | ❌ 否 | ✅ 是 |
复制页表 | ✅ 复制(写时复制) | ❌ 不复制 |
父子并发 | ✅ 并发 | ❌ 父进程阻塞等待子进程执行完 |
安全性 | 高(地址独立) | 低(共享,容易出错) |
速度 | 稍慢 | 更快(节省内存) |
🧠 补充:写时复制(COW)
现代 fork()
实现采用 Copy-On-Write 技术,父子进程共享内存页,只有在子进程写入内存时才真正复制,提高效率。
八、进阶:写时复制(COW)原理
- fork 后父子共享内存页,只读
- 子进程试图写内存 → 触发页保护异常
- 内核复制对应页,子进程写副本
- 父进程继续使用原来的页
🌟 优点:节省时间和内存,尤其适用于 fork 后马上执行 exec 的情况
九、Server 阻塞状态示意
Client Server| ||---- connect() -----> | (accept 阻塞)| |====> 唤醒 accept()
- Server 端
accept()
无客户端连接时处于阻塞状态 - 被连接时唤醒进入 RUNNABLE
- 若使用
epoll
,Server 线程始终活跃,但非阻塞式等待事件
🔚 总结
本教程详尽梳理了线程阻塞与唤醒机制,尤其是 socket 阻塞处理、线程池设计、fork/vfork 差异 等关键知识点。以下是学习建议:
- 掌握阻塞类型:主动、被动、锁等待、IO 等
- 熟练使用 ps/top/jstack 等工具分析线程状态
- 实践线程池模型:自己写一个简易线程池
- 深入理解操作系统中的进程管理机制(fork, vfork, exec)