2025 年 Java 秋招面试必看的 Java 并发编程面试题汇总

简介: 文章摘要: 本文系统梳理Java并发编程核心知识点,助力2025年秋招面试。内容涵盖:1)基础概念,包括线程/进程区别、创建线程的3种方式(Thread/Runnable/Callable)、6种线程状态及转换;2)关键机制,对比sleep()与wait()的锁行为差异,解释start()而非run()启动线程的原因;3)工具类与典型应用场景。通过技术原理与代码示例结合的方式,帮助开发者深入理解并发模型、线程同步等核心问题,为高并发系统设计打下坚实基础。(150字)

2025年Java秋招面试必看的 | Java并发编程 面试题

一、基础概念

1.1 并行与并发的区别

并行是指多个任务在多个CPU核心上同时执行,这是物理上的同时进行。例如,一个拥有多个CPU核心的服务器,不同的核心可以同时处理不同的线程任务。并发则是指多个任务在单CPU核心上交替执行,从逻辑上看好像是同时进行。就像一个服务员在多个顾客之间轮流服务,虽然同一时刻只能服务一个顾客,但通过快速切换,让顾客感觉像是同时被服务。在Java中,通过多线程技术可以实现并发,而并行则依赖于硬件的多核心支持以及合理的线程调度。

1.2 线程的创建方式

1.2.1 继承Thread类

继承Thread类是Java中创建线程的一种方式。通过重写Thread类的run()方法,将线程要执行的任务逻辑写在run()方法中。然后通过创建该类的实例,并调用start()方法来启动线程。例如:

class MyThread extends Thread {
   
    @Override
    public void run() {
   
        System.out.println("线程正在执行:" + Thread.currentThread().getName());
    }
}
public class Main {
   
    public static void main(String[] args) {
   
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

这种方式的优点是简单直观,缺点是由于Java单继承的限制,该类不能再继承其他类。

1.2.2 实现Runnable接口

实现Runnable接口是更为常用的创建线程方式。定义一个类实现Runnable接口,并实现其run()方法。然后将该类的实例作为参数传递给Thread类的构造函数,再调用start()方法启动线程。示例代码如下:

class MyRunnable implements Runnable {
   
    @Override
    public void run() {
   
        System.out.println("线程正在执行:" + Thread.currentThread().getName());
    }
}
public class Main {
   
    public static void main(String[] args) {
   
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();
    }
}

这种方式的好处是避免了单继承的限制,一个类可以同时实现多个接口,更加灵活。

1.2.3 实现Callable接口

实现Callable接口可以让线程有返回值并且能抛出异常。Callable接口中的call()方法定义了线程执行的任务,与Runnable接口的run()方法不同,call()方法有返回值。使用时,需要配合FutureTask类来获取线程执行的结果。示例如下:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
   
    @Override
    public Integer call() throws Exception {
   
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
   
            sum += i;
        }
        return sum;
    }
}
public class Main {
   
    public static void main(String[] args) {
   
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        try {
   
            Integer result = futureTask.get();
            System.out.println("线程执行结果:" + result);
        } catch (InterruptedException | ExecutionException e) {
   
            e.printStackTrace();
        }
    }
}

1.3 线程的状态

Java线程有以下几种状态:

  1. NEW:线程被创建但未启动,例如当我们创建了一个Thread对象,但还未调用其start()方法时,线程处于此状态。
  2. RUNNABLE:线程正在执行或等待CPU资源。当线程调用start()方法后,就进入RUNNABLE状态,此时线程可能正在运行,也可能在等待CPU时间片。
  3. BLOCKED:线程等待锁(进入同步代码块)。当一个线程尝试进入一个被synchronized关键字修饰的代码块,而该代码块的锁已经被其他线程持有时,该线程就会进入BLOCKED状态。
  4. WAITING:线程调用wait()/join()后等待唤醒。例如,当一个线程在同步代码块中调用了Object的wait()方法,它会释放持有的锁并进入WAITING状态,直到其他线程调用notify()或notifyAll()方法唤醒它。
  5. TIMED_WAITING:超时等待(如sleep(long))。线程调用sleep(long millis)方法或者在调用wait(long timeout)方法时指定了超时时间,线程会进入TIMED_WAITING状态,在指定时间过后自动唤醒。
  6. TERMINATED:线程执行完毕或异常终止。当线程的run()方法执行结束或者在执行过程中抛出未捕获的异常时,线程进入TERMINATED状态。

1.4 sleep()与wait()的区别

特性 sleep() wait()
所属类 Thread Object
锁行为 不释放锁 释放锁
唤醒方式 超时自动唤醒 需其他线程调用notify()唤醒
使用场景 线程休眠 线程间通信

sleep()方法是Thread类的静态方法,用于让当前线程暂停执行指定的时间,在休眠期间,线程不会释放它持有的锁。例如:

try {
   
    Thread.sleep(1000); // 线程暂停1秒
} catch (InterruptedException e) {
   
    e.printStackTrace();
}

wait()方法是Object类的方法,必须在同步代码块中使用。当一个线程调用wait()方法时,它会释放当前持有的锁,并进入等待状态,直到其他线程调用该对象的notify()或notifyAll()方法唤醒它。例如:

synchronized (obj) {
   
    try {
   
        obj.wait(); // 线程等待,释放obj锁
    } catch (InterruptedException e) {
   
        e.printStackTrace();
    }
}

1.5 进程与线程的区别

进程是资源分配的最小单位,它拥有独立的内存空间、文件句柄等系统资源。每个进程在运行时,操作系统会为其分配独立的内存区域,不同进程之间的内存空间相互隔离。例如,我们运行的一个Java程序就是一个进程,它有自己独立的堆内存、方法区等。

线程是调度的最小单位,它共享进程的资源,如堆内存、方法区等,但每个线程有自己独立的栈和寄存器。多个线程可以并发执行,提高程序的执行效率。比如在一个Java Web应用中,多个用户的请求可以由不同的线程来处理,这些线程共享应用的堆内存和方法区中的数据。

协程是比线程更轻量级的概念,在Kotlin等语言中有很好的支持。协程可以在一个线程中实现类似多线程的并发效果,通过用户态的调度来切换执行,避免了线程上下文切换的开销。

1.6 为什么用start()而非直接调用run()

当我们直接调用线程的run()方法时,它只是在当前线程中执行run()方法的代码,并没有创建新的线程,也就无法实现多线程并发的效果。例如:

class MyThread extends Thread {
   
    @Override
    public void run() {
   
        System.out.println("线程执行:" + Thread.currentThread().getName());
    }
}
public class Main {
   
    public static void main(String[] args) {
   
        MyThread myThread = new MyThread();
        myThread.run(); // 直接调用run(),在main线程中执行
        System.out.println("main线程执行完毕");
    }
}

而调用start()方法时,Java虚拟机(JVM)会创建一个新的线程,并在这个新线程中执行run()方法,从而实现多线程并发执行。例如:

class MyThread extends Thread {
   
    @Override
    public void run() {
   
        System.out.println("线程执行:" + Thread.currentThread().getName());
    }
}
public class Main {
   
    public static void main(String[] args) {
   
        MyThread myThread = new MyThread();
        myThread.start(); // 启动新线程执行run()
        System.out.println("main线程执行完毕");
    }
}

在上述代码中,当调用start()方法后,会创建一个新线程来执行MyThread的run()方法,同时main线程也会继续向下执行,实现了并发效果。

二、ThreadLocal

2.1 ThreadLocal的作用

ThreadLocal的主要作用是实现线程隔离,为每个线程提供独立的变量副本,避免共享数据在多线程环境下的冲突。例如,在一个Web应用中,每个用户的请求由不同的线程处理,如果需要在整个请求处理过程中存储一些与用户相关的上下文信息,如用户会话ID、用户权限等,使用ThreadLocal就可以方便地为每个线程提供独立的存储区域,各个线程之间的数据互不干扰。

2.2 ThreadLocal的实现原理

每个线程都维护一个ThreadLocal.ThreadLocalMap对象,这个Map的键是ThreadLocal对象,值是线程变量。当一个线程通过ThreadLocal的get()方法获取变量时,它实际上是从自己的ThreadLocalMap中获取对应的值。例如:

public class ThreadLocalExample {
   
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    public static void main(String[] args) {
   
        Thread thread1 = new Thread(() -> {
   
            int value = threadLocal.get();
            value++;
            threadLocal.set(value);
            System.out.println("线程1:" + threadLocal.get());
        });
        Thread thread2 = new Thread(() -> {
   
            int value = threadLocal.get();
            value += 2;
            threadLocal.set(value);
            System.out.println("线程2:" + threadLocal.get());
        });
        thread1.start();
        thread2.start();
    }
}

在上述代码中,thread1和thread2各自维护自己的ThreadLocalMap,它们对threadLocal变量的操作互不影响。

ThreadLocalMap中的键使用弱引用,这是为了防止内存泄漏。当ThreadLocal对象没有其他强引用指向它时,在垃圾回收时,键会被回收。但是如果没有及时调用remove()方法,对应的value可能会因为被ThreadLocalMap中的Entry强引用而无法被回收,从而导致内存泄漏。

2.3 ThreadLocal内存泄漏问题

2.3.1 原因

如前所述,由于ThreadLocalMap中的键是弱引用,当ThreadLocal对象不再被其他地方引用时,在垃圾回收时键会被回收。但如果此时线程没有结束,且没有调用ThreadLocal的remove()方法,那么Entry中的value仍然被Entry强引用,导致value无法被回收,造成内存泄漏。例如:

public class MemoryLeakExample {
   
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
   
        Thread thread = new Thread(() -> {
   
            threadLocal.set("大对象");
            // 这里没有调用threadLocal.remove()
        });
        thread.start();
        // 模拟线程长时间运行,导致ThreadLocal对象没有被及时回收
        try {
   
            Thread.sleep(10000);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
        threadLocal = null; // ThreadLocal对象不再被强引用
        // 此时如果线程没有结束,ThreadLocalMap中的value可能无法被回收,造成内存泄漏
    }
}

2.3.2 解决

为了避免ThreadLocal内存泄漏问题,我们应该在使用完ThreadLocal变量后,在finally块中调用remove()方法,确保及时清理线程中的数据。例如:

public class FixedMemoryLeakExample {
   
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
   
        Thread thread = new Thread(() -> {
   
            try {
   
                threadLocal.set("大对象");
                // 业务逻辑
            } finally {
   
                threadLocal.remove(); // 及时清理
            }
        });
        thread.start();
        try {
   
            Thread.sleep(10000);
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
        threadLocal = null; 
    }
}

通过这种方式,即使ThreadLocal对象不再被强引用,由于已经调用了remove()方法,ThreadLocalMap中对应的Entry也会被移除,避免了内存泄漏。

三、Java内存模型(JMM)

3.1 JMM的核心

Java内存模型(JMM)定义了线程间共享变量的访问规则,其核心围绕解决原子性、可见性和有序性问题。在JMM中,所有的共享变量都存储在主内存中,每个线程都有自己私有的工作内存,线程对变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存读写。例如,当一个线程修改了共享变量的值,首先是在自己的工作内存中修改,然后需要将修改后的值刷新回主内存,其他线程要获取最新值,也需要从主内存中读取。

3.2 JMM的三大特性

3.2.1 原子性

原子性是指操作不可分割,要么全部执行成功,要么全部不执行。例如,对一个int类型变量的赋值操作i = 10;,在单线程环境下是原子操作,但在多线程环境下,如果没有适当的同步机制,可能会出现问题。而使用synchronized关键字修饰的代码块可以保证其原子性,在同一时刻只有一个线程能够进入该代码块执行。例如:

public class AtomicityExample {
   
    private int count = 0;
    public synchronized void increment() {
   
        count++;
    }
}

在上述代码中,increment()方法被synchronized修饰,保证了count++操作的原子性,避免了多线程环境下的竞态条件。

3.2.2 可见性

可见性是指一个线程修改了共享变量的值,其他线程能够立即看到这个修改。在没有同步机制的情况下,线程对变量的修改可能不会及时刷新到主内存,导致其他线程读取到的是旧值。使用volatile关键字可以保证变量的可见性,它强制线程将修改后的值立即刷新到主内存,并且在读取变量时,也会从主内存中读取最新值。例如:

public class VisibilityExample {
   
    private volatile boolean flag = false;
    public void setFlag() {
   
        flag = true;
    }
    public void checkFlag() {
   
        if (flag) {
   
            // 执行相应逻辑
        }
    }
}

在上述代码中,当一个线程调用setFlag()方法修改flag的值后,其他线程调用checkFlag()方法能够立即看到修改后的结果。

3.2.3 有序性

有序性是指程序执行的顺序按照代码的先后顺序执行。但在实际执行中,为了提高性能,编译器和处理器可能会对指令进行重排序。例如:

int a = 10; // 语句1
int b = 20; // 语句2
int c = a + b; // 语句3

在不影响最终结果的情况下,编译器或处理器可能会将语句1和语句2的执行顺序进行重排。使用volatile关键字可以禁止指令重排序,保证特定操作的顺序性。例如:

public class OrderingExample {
   
    private volatile int a = 0;
    private boolean flag = false;
    public void write() {
   
        a = 10; // 语句1
        flag = true; // 语句2
    }
    public void read() {
   
        if (flag) {
    // 语句3
            int result = a * 2; // 语句4
        }
    }
}

在上述代码中,由于flag被volatile修饰,当一个线程执行write()方法时,语句1和语句2的执行顺序不会被重排,并且在另一个线程执行read()方法时,能够保证语句3和语句4的执行顺序是基于正确的a值。

3.3 volatile的作用

3.3.1 可见性

volatile的主要作用之一是保证可见性。当一个变量被volatile修饰时,线程对该变量的写操作会立即刷新到主内存,其他线程对该变量的读操作会从主内存中读取最新值,而不是从自己的工作内存中读取可能的旧值。例如,在一个多线程的计数器场景中:

public class VolatileCounter {
   
    private volatile int count = 0;
    public void increment() {
   
        count++;
    }
    public int getCount() {
   
        return count;
    }
}

多个线程调用increment()方法修改count值,其他线程调用getCount()方法能够获取到最新的count值,避免了由于工作内存数据不一致导致的问题。

3.3.2 有序性

volatile通过内存屏障实现有序性。在写操作后,会插入Store - Barrier指令,强制将修改后的值刷新到主内存,确保对该变量的写操作


Java 并发编程,秋招面试题,2025 秋招,Java 面试,并发面试题,多线程,线程池,ConcurrentHashMap,volatile,synchronized,AQS,Java 内存模型,JUC 包,线程安全,Callable



代码获取方式
https://pan.quark.cn/s/14fcf913bae6


相关文章
|
6月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
265 6
|
6月前
|
IDE Java 编译器
java编程最基础学习
Java入门需掌握:环境搭建、基础语法、面向对象、数组集合与异常处理。通过实践编写简单程序,逐步深入学习,打牢编程基础。
358 1
|
7月前
|
SQL Java 数据库
2025 年 Java 从零基础小白到编程高手的详细学习路线攻略
2025年Java学习路线涵盖基础语法、面向对象、数据库、JavaWeb、Spring全家桶、分布式、云原生与高并发技术,结合实战项目与源码分析,助力零基础学员系统掌握Java开发技能,从入门到精通,全面提升竞争力,顺利进阶编程高手。
1173 2
|
6月前
|
安全 前端开发 Java
从反射到方法句柄:深入探索Java动态编程的终极解决方案
从反射到方法句柄,Java 动态编程不断演进。方法句柄以强类型、低开销、易优化的特性,解决反射性能差、类型弱、安全性低等问题,结合 `invokedynamic` 成为支撑 Lambda 与动态语言的终极方案。
261 0
|
8月前
|
安全 Java 数据库连接
2025 年最新 Java 学习路线图含实操指南助你高效入门 Java 编程掌握核心技能
2025年最新Java学习路线图,涵盖基础环境搭建、核心特性(如密封类、虚拟线程)、模块化开发、响应式编程、主流框架(Spring Boot 3、Spring Security 6)、数据库操作(JPA + Hibernate 6)及微服务实战,助你掌握企业级开发技能。
1014 3
|
7月前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
561 100
|
7月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
7月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
474 16
|
7月前
|
NoSQL Java 关系型数据库
超全 Java 学习路线,帮你系统掌握编程的超详细 Java 学习路线
本文为超全Java学习路线,涵盖基础语法、面向对象编程、数据结构与算法、多线程、JVM原理、主流框架(如Spring Boot)、数据库(MySQL、Redis)及项目实战等内容,助力从零基础到企业级开发高手的进阶之路。
504 1