1. 进程与线程的关系
进程是操作系统分配资源的基本单位,每个进程有独立的内存空间、文件句柄等资源。线程只能寄生在进程之中,不能单独存在。作为一个热爱Java的哈基米,让我们看看Java怎么定义线程的。在 Oracle 官方 Java SE 21 文档里(Java Platform, SE 21 API Specification)对线程有个精炼的定义:
A thread is a thread of execution in a program. The Java Virtual Machine allows an application to have multiple threads of execution running concurrently.
简单翻译:线程就是程序中的执行流,JVM 允许同时跑多个执行流。
在 Stack Overflow的一个问题 “Difference between Process and Thread” 里,有很多关于进程和线程区别的讨论,感兴趣的可以看看。这里给出高赞的一个回答:
进程: 每个进程都提供了执行程序所需的资源。一个进程拥有虚拟地址空间、可执行代码、系统对象的打开句柄、安全上下文、唯一的进程标识符、环境变量、优先级类别、最小和最大工作集大小,以及至少一个执行线程。每个进程启动时都带有一个主线程(通常称为主线程),但可以从其任意线程中创建其他线程。
线程: 线程是进程内的一个可调度执行实体。进程的所有线程共享其虚拟地址空间和系统资源。每个线程都维护异常处理程序、调度优先级、线程本地存储、唯一的线程标识符,以及一组系统用于保存线程上下文直至其被调度的数据结构。线程上下文包括线程的机器寄存器组、内核堆栈、线程环境块,以及线程所属进程地址空间中的用户堆栈。线程还可以拥有自己的安全上下文,用于模拟客户端身份。
Microsoft Windows支持抢占式多任务处理,这种机制实现了多个进程中多个线程同时执行的效果。在多处理器计算机上,系统可以同时执行的线程数量与计算机的处理器数量相同。
2. 状态的转换
进程和线程都不是“生下来就一直干活”,就像一个人一天会经历“工作 → 休息 → 等待 → 被叫醒 → 再工作”。进程和线程都有一套状态的转换机制。
进程的经典状态
- 新建(New):刚刚被 fork 出来,还没开始跑。
- 就绪(Ready):万事俱备,只等 CPU。
- 运行(Running):CPU 时间片轮到它。
- 阻塞(Blocked / Waiting):等 I/O、等锁或者等信号。
- 终止(Terminated):生命周期结束。
- 阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
- 就绪挂起状态:进程在外存(硬盘),但只要进入内存,即刻立刻运行。
线程的经典状态
线程的状态在 Java 里对应 Thread.State
枚举:
public enum State {
/**
尚未启动的线程状态
*/
NEW,
/**
可运行线程状态。处于可运行状态的线程正在Java虚拟机中执行,但可能在等待操作系统提供的处理器等资源
*/
RUNNABLE,
/**
因等待监视器锁而被阻塞的线程状态。处于阻塞状态的线程正在等待监视器锁以进入同步块/方法,或在调用Object.wait()后重新进入同步块/方法
*/
BLOCKED,
/**
等待状态的线程。线程因调用以下方法之一而进入等待状态:
• Object.wait()无超时;
• Thread.join()无超时;
• LockSupport.park();
处于等待状态的线程正在等待另一个线程执行特定操作。
*/
WAITING,
/**
具有指定等待时间的等待线程状态。线程因调用以下带有正数等待时间的方法之一而进入定时等待状态:
• Thread.sleep()
• Object.wait(long)带超时
• Thread.join(long)带超时
• LockSupport.parkNanos()
• LockSupport.parkUntil()
*/
TIMED_WAITING,
/*
已终止的线程状态。线程已完成执行
*/
TERMINATED;
}
注意:这里的 RUNNABLE
并不等于“正在运行”,而是处于就绪队列中等着 CPU 分配时间片。
3. 进程间通信(IPC)
管道(Pipe):管道是单向字节流,它将一个进程的标准输出连接到另一个进程的标准输入。
- 命名管道(FIFO):多进程可按名字访问的管道
- 命名管道(FIFO):多进程可按名字访问的管道
消息队列(Message Queue):消息队列允许一个或多个进程写入消息,这些消息将被一个或多个读取进程接收。
信号(Signal):信号是Unix系统使用的最古老的进程间通信方式之一。信号可以由键盘中断或错误条件(如进程尝试访问其虚拟内存中不存在的地址)触发。内核或系统中的其他进程可以生成一组预定义的信号。例如,按下
Ctrl+C
会向进程A发送SIGINT
信号。
信号量(Semaphore):信号量是内存中的一个位置,其值可以被多个进程测试和设置。根据测试和设置操作的结果,某个进程可能需要休眠,直到另一个进程修改信号量的值。
共享内存(Shared Memory):共享内存允许多个进程通过映射到各自虚拟地址空间的内存进行通信。当进程不再需要共享该内存时,可以解除映射。
套接字(Socket):不仅本机进程可用,跨网络也能通信。
4. 调度算法
就像银行叫号一样,操作系统得决定哪个进程或线程先到CPU执行(被叫到柜台办理业务):
- 先来先服务(FCFS):按到达顺序调度。
- 最短作业优先(SJF):短任务先上,但需要预估执行时间。
- 时间片轮转(RR):每个分一小段 CPU 时间,公平但频繁切换有开销。
- 优先级调度:权重高的先执行,但要防止低优先级“饿死”。
- 多级队列调度:将进程按优先级或类型分组,并在不同队列中采用不同调度策略的算法,每个队列可以有自己的调度方式(如先来先服务FCFS、时间片轮转RR等),系统按照预设规则在队列之间分配CPU资源。
Java 线程的优先级会影响操作系统调度吗?
理论上会(Thread#setPriority
),但在现代多核 OS 上,这个优先级只是个建议值,不保证生效。
5. 死锁
在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
死锁要同时满足以下四个必要条件才会发生:
- 互斥:多个线程不能同时使用同一个资源
- 持有并等待:持有资源时还想申请新资源
- 不可剥夺:资源不能被强制拿走
- 环路等待:形成资源请求的环
解决死锁的方式有三类:预防、避免、检测+恢复。在 Java 里可以用 ThreadMXBean
去检测:
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] ids = bean.findDeadlockedThreads();
if (ids != null) {
ThreadInfo[] infos = bean.getThreadInfo(ids);
Arrays.stream(infos).forEach(System.out::println);
}