1.并发编程(上)
1.何为进程和线程?
1.1 何为进程?
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在Java中,我们启动main函数是启动类JVM的进程,其中main函数所在的线程就是该进程的主线程。
1.2 何为线程?
线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程或各个线程之间作切换工作时,负担要比进程小得多,因此,线程也被称为轻量级进程。
注意:一个Java程序的运行是main线程与其他多个线程同时运行。
- 一对一:一个用户线程对应一个内核线程
- 多对一:多个用户线程映射一个内核线程
- 多对多:多个用户线程映射多个内核线程
在Windows和Linux环境中,采用的线程模型是一对一的现成模型,即一个用户线程对应一个系统内核线程。
2.进程与线程的关系、区别及优缺点?
在图中,一个进程有多个线程,多个线程共享进程的堆、方法区(JDK1.8之后的元空间)资源,每个线程都有自己的程序计数器、本地方法栈以及虚拟机栈。
1.1 程序计数器为什么是私有的?
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,指向线程上下文切换的返回值。
1.2 本地方法栈和虚拟机栈为什么是私有的?
- 虚拟机栈: 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
1.3 堆和方法区的作用
2.为什么要使用多线程以及其带来的问题
2.1 为什么使用多线程?
- 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- 从当代互联网发展趋势来说: 市场所需,现在的系统高并发系统要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
- 单核时代:在单核时代多线程主要是为了提高单进程利用 CPU 和 IO 系统的效率。 假设只运行了一个 Java 进程的情况,当我们请求 IO 的时候,如果 Java 进程中只有一个线程,此线程被 IO 阻塞则整个进程被阻塞。CPU 和 IO 设备只有一个在运行,那么可以简单地说系统整体效率只有 50%。当使用多线程的时候,一个线程被 IO 阻塞,其他线程还可以继续使用 CPU。从而提高了 Java 进程利用系统资源的整体效率。
- 多核时代: 多核时代多线程主要是为了提高进程利用多核 CPU 的能力(充分利用CPU资源)。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,不论系统有几个 CPU 核心,都只会有一个 CPU 核心被利用到。而创建多个线程,这些线程可以被映射到底层多个 CPU 上执行,在任务中的多个线程没有资源竞争的情况下,任务执行的效率会有显著性的提高,约等于(单核时执行时间/CPU 核心数)。
2.2 多线程带来的问题
线程安全与否是指在多线程环境下,同一数据的访问是否能保证其正确性和一致性。线程安全是指对于同一份数据,不管有多少线程同时访问,都能保证数据的正确性和一致性。反之则是多线程环境下,多个线程同时运行会造成数据混乱、错误、或者丢失。
3.单核CPU运行多个线程效率高吗
单核 CPU 同时运行多个线程的效率取决于线程的类型和任务的性质。线程可分为:CPU 密集型和 IO 密集型。
- CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。
- IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。
在单核 CPU 上,同一时刻只能有一个线程在运行,其他线程需要等待 CPU 的时间片分配。如果线程是 CPU 密集型的,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了程序运行效率。如果线程是 IO 密集型的,那么多个线程同时运行可以利用 CPU 在等待 IO 时的空闲时间,提高了效率。
4.线程的生命周期和状态
- NEW: 初始状态,线程被创建出来但没有被调用
start()
。 - RUNNABLE: 运行状态,线程被调用了
start()
等待运行的状态。 - BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
从上图可以了解到线程的生命周期:
- 当线程被创建出来,但还没被start()方法调用的时候处于NEW状态(初始状态);
- 当NEW状态的线程被start()调用,但还未获得时间片的时候处于REAYDY状态(可运行状态);
- 当READY状态的线程获得时间片后进行RUNNABLE状态(运行状态);
- 当RUNNABLE的线程调用Object.wait()、join()、park()方法的时候,线程进入WAITING状态。进入到等待状态的线程需要等待其它线程的通知才能返回到运行状态(此时是进入READY状态)。
- TIMED_WAITING状态相当于在WAITING状态加个时间限制,如
sleep(long millis)
方法或wait(long millis)
方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。 - 当线程进入
synchronized
方法/块或者调用wait
后(被notify
)重新进入synchronized
方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态 - 线程在执行完了
run()
方法之后将会进入到 TERMINATED(终止) 状态
5.线程上下文切换
- 主动让出 CPU,比如调用
sleep()
,wait()
等方法。 - 时间片用完,因为操作系统要防止一个线程或者进程长时间占用 CPU 导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行。
其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场,并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
6.线程死锁、如何避免
6.1 线程死锁
代码演示:

public class DeadLockDemo1 { private static Object resource1 = new Object(); private static Object resource2 = new Object(); public static void main(String[] args) { new Thread(()->{ synchronized (resource2){ System.out.println(Thread.currentThread().getName()+"持有resource2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"等待resource1"); synchronized (resource1){ System.out.println(Thread.currentThread().getName()+"持有resource1"); } } }, "thread2").start(); new Thread(()->{ synchronized (resource1){ System.out.println(Thread.currentThread().getName()+"持有resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"等待resource2"); synchronized (resource2){ System.out.println(Thread.currentThread().getName()+"持有resource2"); } } }, "thread1").start(); } }
运行结果:
thread2持有resource2
thread1持有resource1
thread1等待resource2
thread2等待resource1
产生死锁的四个条件:
- 互斥条件:任意时刻资源只能由一个线程占有;
- 请求并等待:一个线程获取资源时阻塞,对已有资源不释放;
- 不剥夺条件:线程在已获得的资源未使用完之前不能被其他线程剥夺,只能自己使用完毕后才释放;
- 循环等待:若干线程形成一条首尾相连的循环资源等待链路。
6.2 如何避免线程死锁
- 请求并等待:一次性申请所有资源。
- 不剥夺条件:占用一部分资源的线程,如果申请不到其他所需资源时,释放自己所持有的资源。
- 循环等待:按序申请资源。
使用按序申请资源原则,修改上述死锁演示代码中的thread2线程申请资源的顺序:
代码展示:

public class DeadLockDemo2 { private static Object resource1 = new Object(); private static Object resource2 = new Object(); public static void main(String[] args) { new Thread(()->{ synchronized (resource1){ System.out.println(Thread.currentThread().getName()+"持有resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"等待resource2"); synchronized (resource2){ System.out.println(Thread.currentThread().getName()+"持有resource2"); } } }, "thread2").start(); new Thread(()->{ synchronized (resource1){ System.out.println(Thread.currentThread().getName()+"持有resource1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"等待resource2"); synchronized (resource2){ System.out.println(Thread.currentThread().getName()+"持有resource2"); } } }, "thread1").start(); } }
运行结果:
thread2持有resource1
thread2等待resource2
thread2持有resource2
thread1持有resource1
thread1等待resource2
thread1持有resource2
7.sleep()方法和wait()方法对比
- sleep()方法调用没有释放锁,wait()方法调用会释放锁;
- sleep()方法通常用于线程暂停,wait()方法通常用于线程通信;
- 调用了sleep()方法的线程会自动苏醒,调用了wait()方法的线程需要其他线程通过notify()或notifyAll()方法唤醒;
- sleep()方法是Thread的静态本地方法,wait()方法是Object类的本地方法。
7.1 为什么wait()方法不定义在Thread类中?
每个对象(Object)都有自己的对象锁,wait()方法的作用是让获取锁的线程等待,自动释放当前线程所持有的锁并进入WAITING状态。那么操作的会是对象(Object)而非Thread类。
7.2 为什么sleep()方法不定义在Object中?
sleep()方法的作用是使线程暂停,不涉及对象,也不需要获得对象锁。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· .NET10 - 预览版1新功能体验(一)