JavaEE 线程知识点全面梳理(上)
前言
在 JavaEE 开发领域,线程是一个至关重要的概念,它与程序的性能、并发处理能力密切相关。本文将全面梳理 JavaEE 线程的相关知识点,从基础概念到高级特性,从理论知识到实践应用,帮助大家系统复习,从容应对面试挑战。
一、认识线程
1.线程是什么?
一个线程就是一个执行流,每个线程会按照顺序单独运行自己的代码。多个线程之间可以“同时”运行多份代码。
2.为什么需要线程?
我们首先得了解并发编程,自计算机诞生,程序往往通过串行化执行,只有一个任务完成,才能执行下一个任务,但是这样效率低下,随着需求增加,通过设计机制让多个任务 “同时” 推进,最大化利用硬件资源,满足复杂场景的需求。更完全的利用了cpu高效处理,让多个任务在cpu上交错执行,宏观上实现同时运行,避免了cpu的闲置,以及大幅度提升处理效率。
虽然多进程也能实现并发编程,但是线程比进程更轻量级
- 1.创建速度比进程快
- 2.销毁速度比进程快
- 3.调度速度比进程快
3.线程和进程区别
-
1.进程包含线程。
-
2.每个进程都有一个主线程。
-
3.当一个线程崩溃可能会影响同个进程的线程,而进程之间互不影响。
-
4.进程是系统分配资源的最小单位,线程是cpu调度的最小单位。
-
5.进程通信复杂,需依赖操作系统提供的机制(如管道、消息队列、共享内存、Socket 等)。线程通信简单,可直接读写进程内的共享内存(如全局变量、共享缓冲区),也可使用线程间同步机制(互斥锁、信号量等)
4.Java中线程和操作系统的线程有什么区别
线程是操作系统的概念,操作系统内核实现了这个功能(如 Linux 的pthread),并且提供一些API供用户使用,而Java标准库Thread类可以视为对操作系统提供的API进一步的抽象和封装,方便使用。
二、线程的创建及使用
- 每个线程都是独立执行的流
- 每个线程之间是并发执行的
1.继承 Thread 类
步骤:
1.定义一个类继承Thread类
2.重写Thread类中的run方法
3.创建该类的实例对象
4.通过实例对象的start()方法启动线程
代码如下(示例):
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread " + Thread.currentThread().getId() + ": " + i);
}
}
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
}
}
优缺点:
- 优点:实现简单,直接继承 Thread 类即可。
- 缺点:由于 Java 单继承的特性,该类无法再继承其他类,灵活性较差。
2.实现Runnable接口
步骤:
1.定义一个类实现Runnable接口
2.重写Runnable接口的run方法,在run方法中定义线程的任务。
3.创建该类的实例对象
4.将该类的实例对象作为参数传给Thread类的构造方法,构建Thread类实例。
5.调用Thread类实例的start()方法启动该线程
代码如下(示例):
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread " + Thread.currentThread().getId() + ": " + i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
优缺点:
- 优点:避免了单继承的限制,一个类可以实现多个接口,灵活性更高;适合多个线程共享同一个资源的场景。
- 缺点:相对于继承 Thread 类,实现 Runnable 接口需要多创建一个 Runnable 对象,稍显繁琐。
3.其他变形
- 匿名内部类创建Thread子类对象
public class ThreadTest2 {
public static void main(String[] args) {
Thread thread=new Thread(){
@Override
public void run() {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
}
};
thread.start();
}
}
- 匿名内部类创建Runnable子类对象
public class RunnableTest2 {
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
}
});
thread.start();
}
}
- lambda表达式创建Runnable子类对象
public class RunnableTest3 {
public static void main(String[] args) {
Thread t1=new Thread(()->{
System.out.println("t1:使⽤匿名类创建 Thread ⼦类对象");
});
Thread t2=new Thread(()->{
System.out.println("t2:使⽤匿名类创建 Thread ⼦类对象");
});
t1.start();
t2.start();
}
}
三、Thread类及其常见的方法
Thread类是JVM用于管理线程的一个类,每个Thread类都有一个Thread对象与之相关联。Thread类的对象是 Java 中对线程执行流的抽象描述,而 JVM 会对这些对象进行统一管理,以实现线程的调度和生命周期控制。
1.Thread常见的构造方法
以下是你需要的原始 Markdown 表格语法,可直接复制使用:
| 构造方法 | 说明 |
|---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为这个目前我们了解即可(原文表述略残缺,按图中呈现) |
2.Thread的几个常见属性
以下是转换为 Markdown 格式的内容,可直接复制使用:
| 属性 | 获取方法 | 说明 |
|---|---|---|
| ID | getId() | 线程的唯一标识,不同线程不会重复 |
| 名称 | getName() | 供调试工具使用,便于识别线程 |
| 状态 | getState() | 表示线程当前所处情况,后续进一步说明 |
| 优先级 | getPriority() | 优先级高的线程理论上更容易被调度 |
| 是否后台线程 | isDaemon() | JVM 会在一个进程的所有非后台线程结束后,才会结束运行 |
| 是否存活 | isAlive() | 简单理解为 run 方法是否运行结束 |
| 是否被中断 | isInterrupted() | 1.通过共享的标记进行沟通调用 (volatile) 2.interrupt() 方法来通知 |
这是 Java 中 Thread 类的 join 方法说明
| 方法 | 说明 |
|---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,可更高精度控制等待时长(纳秒级补充) |
这是 Java 中 Thread 类的 sleep 方法说明
| 方法 | 说明 |
|---|---|
public static void sleep(long millis) throws InterruptedException | 使当前线程休眠指定的 millis 毫秒时长(1 毫秒 = 1000000 纳秒 ),休眠期间线程会放弃 CPU 执行权,进入阻塞状态;若休眠过程中线程被中断(比如其他线程调用 interrupt 方法),会抛出 InterruptedException 异常并提前唤醒线程 |
public static void sleep(long millis, int nanos) throws InterruptedException | 使当前线程休眠更高精度的时长,millis 表示毫秒数,nanos 表示纳秒数(需满足 0 <= nanos < 1000000 ),作用和处理中断的逻辑与上面方法一致,只是能更精细控制休眠时间 |
3.线程的运行周期及转换状态
- NEW(新建状态):线程已被创建,但尚未调用start()方法,即安排了工作,还未开始行动。
- RUNNABLE(可运行状态):调用start()方法后线程所处的状态,可工作的,又可以分成正在工作中和即将开始工作两种情况。
- BLOCKED(阻塞状态):线程在运行过程中因某些原因(如获取锁失败等)进入阻塞,处于排队等着其他事情的状态。
- WAITING(等待状态):线程因调用wait()等方法进入等待,处于排队等着其他事情的状态,需其他线程唤醒。
- TIMED_WAITING(计时等待状态):线程因调用sleep(long millis)、wait(long
timeout)等方法进入计时等待,处于排队等着其他事情的状态,等待时间结束或被唤醒后可继续执行。 - TERMINATED(终止状态):线程的run()方法执行完毕或因异常终止,即工作完成了。
小结:创建Thread对象这一步骤并不是真的在操作系统创建线程实体,为了方便管理,只是在JVM层面实例一个线程对象,存储线程的元数据(名称、优先级、要执行的任务等)。只有调用start()方法,JVM才会在调用本地API,在操作系统内核中真正创建线程实例,并将线程设置为RUNNABLE状态,等待CPU调度执行.
- 创建Thread对象->构造建房图纸(设计状态)
- 调用start()方法->根据图纸正式开始修建
四、线程安全
1.线程不安全的现象
以下代码案例:让两个线程分别对count相加50000次
public class ThreadSecure {
private static int count=0;
public static void main(String[] args) throws InterruptedException {
// 构建t1线程
Thread t1=new Thread(){
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
count++;
}
}
};
// 构建t2线程
Thread t2=new Thread(){
@Override
public void run() {
for (int i = 0; i <50000 ; i++) {
count++;
}
}
};
// 启动线程
t1.start();
t2.start();
// 等待线程执行完成
t1.join();
t2.join();
System.out.println("经过计算:count="+count);
}
}

可以看到,这个计算是错误的,多次运行结果不统一,也都达不到我们预期结果。
若多线程环境下,代码运行结果与单线程环境应得结果一致,该程序 线程安全 。
2.线程不安全原因
-
1.线程是CPU随机调度的(执行顺序不可控)
-
2.操作不具备原子性(多步操作可能被拆分执行)
-
3.共享变量可见性问题(线程间数据更新无法及时感知)
-
4.指令重排序(编译器/CPU 为了优化导致执行顺序与预期不符合)
原子性:
一条 Java 语句不一定是原子的,也不一定只对应一条指令。例如n++,实际由三步操作组成:
1.从内存把数据读到 CPU
2.进行数据更新
3.把数据写回内存
不保证原子性给多线程带来的问题:
若一个线程正在对变量操作时,还没来得及写回内存,其他线程中途插入,进行数据读取,导致读取原先的数据,操作被打断,可能产生错误结果。
共享变量可见性问题:
每个线程有自己单独的工作内存,并且存在缓存机制

3.解决线程不安全的措施
①volatile关键字
volatile修饰的变量,通过内存屏障机制,同时解决内存可见性和指令重排序问题。
1.解决内存可见性:
写操作:修改 volatie 修饰的变量后,会立即将新值从工作内存中刷新到主内存中
读操作:读取 volatile 修饰的变量后,会先失效线程工作内存中的缓存,强制从主内存加载最新值。
2.禁止指令重排序
volatile 通过插入特定内存屏障,阻止编译器和 CPU 对指令进行无序优化:
写屏障:在 volatile 变量写操作后插入,禁止后续指令重排序到写操作之前。
读屏障:在 volatile 变量读操作前插入,禁止前面的指令重排序到读操作之后。
这确保了代码的执行顺序与源码逻辑一致,避免多线程环境下因重排序导致的逻辑混乱(例如单例模式中的双重检查锁场景)。
注意volatile关键字,并不能解决原子性问题
②synchronized关键字-加锁
1.认识synchronized锁
在Java中,synchronized是一个关键字,用于实现在多线程之间的同步,确保多个线程在访问共享资源时不会出现数据不一致问题。它解决了操作不具备原子性问题。
锁的本质: synchronized本质是对象监视器,每一个Java对象都内置了一个Monitor。当线程进入同步代码块时,必须获取对象的Monitor锁,退出时才会释放锁,未获取到锁的线程会进入阻塞队列,直到锁被释放。
2. synchronized的特性
1.互斥性: 当一个线程进入某个对象的synchronized同步代码(包括同步方法或同步代码块)时,若其他线程也尝试进入同一个对象synchronized同步代码,由于该对象的锁已被占用,这些线程将无法获取锁,只能进入阻塞状态并等待。
2.可重入: synchronized 具备可重入性,指的是同一线程可以多次获取同一把锁,而不会因自己已持有该锁而产生阻塞。
JVM 会通过一个锁计数器记录线程获取锁的次数:
- 线程首次获取锁时,计数器值为 1。
- 同一线程再次获取该锁时,计数器值递增(如 2、3…)。
- 线程退出同步代码时,计数器值递减;当计数器值为 0 时,才真正释放锁。
3.synchronized使用方式
synchronized本质上要修改对象的对象头,因此 synchronized 的使用必须搭配一个具体的对象。
①修饰代码块:明确指定锁对象
锁任意对象
public class SynchronizedDemo {
private Object locker=new Object();
public void method(){
synchronized (locker){
}
}
}
锁当前类的实例对象:
// 写法1:
public class SynchronizedDemo {
public void method(){
synchronized (this){
}
}
}
// 写法2:直接修饰普通方法
public class SynchronizedDemo {
public synchronized void methond() {
}
}
修饰静态方法
public class SynchronizedDemo {
public synchronized static void method() {
}
}
五、面试常考点
1.wait和sleep区别
-
设计目的不同
wait()是object类的方法,专为线程间协作设计,是用于线程之间的通信。
sleep()是Thread类的静态方法,仅用于让线程暂时休息一段时间,不涉及线程之间的交互。 -
对锁的处理不同
wait()必须在synchronized修饰的同步代码块/同步方法中调用(持有锁),且调用后会主动释放锁,让其他线程有机会获取到锁.
sleep()不依赖锁,可以在任何地方调用,且不释放锁资源,其他线程无法获取到该线程持有的锁。 -
唤醒方式不同
wait()不会自动唤醒,必须其他线程用同一个对象的notify()或notifyAll()方法才能被唤醒,也可以通过interrupt()中断。
sleep()是定时休眠,时间到后自动唤醒,也可以被interrupt()中断。 -
异常处理方式不同
wait()虽然也可能抛出该异常,但不一定强制捕获
sleep()必须强制捕获InterruptedException
2.start和run区别
| 对比维度 | start() | run() |
|---|---|---|
| 作用 | 启动新线程(JVM调用本地API创建OS线程),将线程设为RUNNABLE | 仅执行线程任务的普通方法(无新线程创建) |
| 调用效果 | 多次调用会抛IllegalThreadStateException | 可多次调用(和普通方法一样) |
| 线程状态变化 | NEW → RUNNABLE | 无状态变化(线程仍为原状态) |
3.线程状态转换关系
- NEW → RUNNABLE:仅start()方法触发(JVM 创建 OS 线程);
- RUNNABLE → BLOCKED:仅因「竞争 synchronized 锁失败」(区别于 Lock 锁的 WAITING 状态);
- RUNNABLE → WAITING:调用无参wait()、无参join();
- RUNNABLE → TIMED_WAITING:调用sleep(long)、有参wait(long)、有参join(long);
- 所有阻塞状态 → TERMINATED:线程异常终止(如未捕获的 Exception)。
4.线程与进程之间区别
一、核心结论
进程和线程是操作系统管理任务执行的核心概念,最本质区别在于:
- 进程是「操作系统资源分配的基本单位」(管理资源);
- 线程是「CPU 调度与执行的基本单位」(负责执行);
- 线程依赖进程存在,共享进程资源,是进程的 “执行片段”。
二、6 大核心维度对比
| 对比维度 | 进程(Process) | 线程(Thread) | 关键补充说明 |
|---|---|---|---|
| 1. 资源占用 | 占用独立资源(内存空间、文件描述符、全局变量等),资源消耗大 | 共享所属进程资源,仅拥有私有资源(程序计数器 PC、线程栈、寄存器),资源消耗极小 | 进程间内存完全隔离(如微信和 QQ 不能直接访问对方内存);同一进程的线程可共享堆内存(如 Java 多线程操作同一对象) |
| 2. CPU 调度开销 | 调度时需切换「进程上下文」(保存 / 恢复全部资源状态),开销大(约 1000 个 CPU 周期) | 调度时仅切换「线程上下文」(仅私有资源),开销小(约几十 CPU 周期) | 线程调度效率远高于进程,是多并发场景的首选 |
| 3. 独立性 | 独立性强,进程间相互隔离,一个进程崩溃不影响其他进程 | 独立性弱,依赖进程;同一进程内某线程崩溃(如空指针),可能导致整个进程退出 | 例:Chrome 浏览器 “每标签页一个进程”,某标签卡死不影响其他标签;而同一进程的线程崩溃会让整个程序闪退 |
| 4. 通信方式 | 需依赖操作系统 IPC 机制(管道、消息队列、共享内存、Socket 等),复杂且效率低 | 可直接读写进程内共享资源(如全局变量),或用同步机制(锁、信号量),简单且效率高 | 线程间通信无需跨 “资源隔离边界”,比进程间通信(IPC)快 10 倍以上 |
| 5. 创建 / 销毁成本 | 成本高(需分配内存、初始化资源、注册进程表),创建速度慢 | 成本低(仅初始化私有资源,复用进程资源),创建速度快 | 创建 1 个进程的时间约是创建 1 个线程的 10-100 倍,高频创建任务(如 Web 服务器处理请求)必用线程 |
| 6. 数量上限 | 系统支持的进程数量少(受内存、进程表限制,通常几万级) | 系统支持的线程数量多(资源消耗小,通常几十万级) | 例:Linux 系统进程上限约 3 万,线程上限可配置到 10 万 + |
三、关联关系与应用场景
1.进程与线程的关联
- 线程不能独立存在,必须属于某个进程;
- 一个进程至少有 1 个主线程(如 Java 的main线程),可创建多个工作线程;
- 进程的资源(内存、文件)由其内部所有线程共享,线程仅私有少量执行相关资源。
2.场景选择建议
- 用多进程:需强隔离、避免崩溃扩散的场景(浏览器标签页、Docker 容器、虚拟机);
- 用多线程:需高频并发、低开销的场景(Web 服务器处理请求、APP 后台刷新、计算密集型任务(如大数据排序))。
更多推荐

所有评论(0)