前言

       在 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 格式的内容,可直接复制使用:

属性获取方法说明
IDgetId()线程的唯一标识,不同线程不会重复
名称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. 线程首次获取锁时,计数器值为 1。
  2. 同一线程再次获取该锁时,计数器值递增(如 2、3…)。
  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区别

  1. 设计目的不同
    wait()是object类的方法,专为线程间协作设计,是用于线程之间的通信
    sleep()是Thread类的静态方法,仅用于让线程暂时休息一段时间,不涉及线程之间的交互。

  2. 对锁的处理不同
    wait()必须在synchronized修饰的同步代码块/同步方法中调用(持有锁),且调用后会主动释放锁,让其他线程有机会获取到锁.
    sleep()不依赖锁,可以在任何地方调用,且不释放锁资源,其他线程无法获取到该线程持有的锁。

  3. 唤醒方式不同
    wait()不会自动唤醒,必须其他线程用同一个对象的notify()或notifyAll()方法才能被唤醒,也可以通过interrupt()中断。
    sleep()是定时休眠,时间到后自动唤醒,也可以被interrupt()中断。

  4. 异常处理方式不同
    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 后台刷新、计算密集型任务(如大数据排序))。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐