Java -- 并发编程

文章目录

面试必问之并发编程

多线程原理

并发和并行

何为并发?什么是并发和并行?

  • 并发(Concurrency)👉 是指多个任务在同一时间段内交替执行,宏观上看起来是同时进行的,但微观上是交替执行的。就像一个人同时处理多项任务,实际上是在不同任务间快速切换。
  • 并行(Parallelism)👉 是指多个任务在同一时刻真正同时执行,每个任务都有独立的执行单元。就像多个人各自负责一项任务同时工作。

想象你在厨房做饭:

  • 并发:一个厨师同时炒三个菜,不停地在三个锅之间来回切换。
  • 并行:三个厨师,每人负责一个菜,同时工作。

进程和线程

多线程是什么?什么是进程、什么是线程?

  • 进程📦 是操作系统分配资源的基本单位,代表一个正在执行的程序实例。每个进程都有独立的内存空间和系统资源。
  • 线程🧵 是CPU调度执行的基本单位,是进程中的一条执行路径。一个进程可以包含多个线程,它们共享进程的内存空间和资源。

打个比喻,你在打一把王者:

  • 进程可以比作是你开的这一把游戏。
  • 线程可以比作是你所选的英雄或者是游戏中的水晶野怪等之类的。

同步和异步

  • 同步👫:是指在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
  • 异步🚻:则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

举例来说明:

  • 同步就像是打电话:不挂电话,通话不会结束。
  • 异步就像是发短信:发完短信后,就可以做其他事;当收到回复短信时,手机会通过铃声或振动来提醒。

阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态:

  • 阻塞:是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
  • 非阻塞:是指在不能立刻得到结果之前,该调用不会阻塞当前线程。

举例来说明:

  • 阻塞调用就像是打电话,通话不结束,不能放下。
  • 非阻塞调用就像是发短信,发完短信后,就可以做其他事,短信来了,手机会提醒。

什么是上下文切换?

频繁切换会带来额外开销 ,影响系统性能。什么是上下文切换?

上下文切换 是指CPU从一个进程或线程切换到另一个进程或线程时,需要保存当前任务的状态恢复目标任务状态的过程。

  • 什么时候发生:时间片用完、I/O阻塞、高优先级任务抢占
  • 保存什么:CPU寄存器、程序计数器、栈指针等上下文信息
  • 优化方向:减少不必要的线程、使用协程、合理设置线程池大小

举例来说明:

  • 在厨房做饭时,突然有人敲门,你得先记住炒菜进行到哪一步(保存现场);
  • 去开门处理事情;
  • 回来后再继续刚才的步骤(恢复现场)。

⭐️Java线程的6种状态及切换

从线程的创建到终止及状态转换。线程的生命周期有哪些状态及如何流转

在Java中,线程的生命周期主要包含6个状态,分别是:

  • NEW**(新建)**:线程对象已创建但尚未启动
  • RUNNABLE**(可运行)**:线程已启动,进入就绪状态,正在执行或等待CPU资源
  • BLOCKED**(阻塞)**:线程因等待锁而被阻塞
  • WAITING**(等待)**:线程进入无限期等待状态
  • TIMED_WAITING**(定时等待)**:线程进入有限期等待状态
  • TERMINATED**(终止)**:线程执行完毕或者因异常退出后

💡 面试技巧:回答这个问题时,最好结合Thread.State枚举类来说明,并简单描述各状态间的转换关系,展现你对线程状态管理的理解。

创建线程的方式

创建线程不止4种方式,虽然它们都是依赖 new Thread(),但是多点思考和理解,多点差异化,给面试官不一般的感受。创建线程的几种方式

  1. 继承Thread类;
  2. 实现Runnable接口;
  3. 实现Callable接口;
  4. 使用ExecutorService线程池;
  5. 使用CompletableFuture类;
  6. 基于ThreadGroup线程组;
  7. 使用FutureTask类;
  8. 使用匿名内部类或Lambda表达式;
  9. 使用Timer定时器类;
  10. 使用ForkJoin线程池或Stream并行流。

线程安全

多线程环境中就会出现意料之外的结果,即线程不安全。什么是线程安全,如何实现线程安全

实现线程安全的主要方式有:

  1. 使用同步机制 :通过 synchronized 关键字或 Lock 接口实现对共享资源的互斥访问
  2. 使用线程安全的集合类 :如 ConcurrentHashMapCopyOnWriteArrayList
  3. 使用原子类 :如 AtomicIntegerAtomicReference 等确保操作的原子性
  4. 使用 volatile 关键字:保证变量的可见性
  5. 采用线程本地存储 :使用 ThreadLocal 避免共享资源
  6. 使用不可变对象 :如 String,天生就是线程安全的

并发的三大特性

原子性、可见性、有序性。 什么是原子性、可见性、有序性?

  1. 原子性(Atomicity)
    • 一个操作是不可分割的,要么全部执行成功,要么全部不执行,不存在中间状态。
    • 原子操作通常是通过 机制来实现的,或者使用原子类型(如AtomicInteger)。在 Java 中,还可以通过synchronized关键字、Lock锁以及CAS(比较并交换)来保证原子性
  2. 可见性(Visibility)
    • 一个线程修改共享变量时,其他线程能立即看到最新值。
    • 可见性通常通过使用volatile关键字或者使用 来保证。volatile关键字可以确保一个变量的修改对其他线程是可见的。此外,synchronized关键字和Lock锁在保证原子性的同时,也会做一个与主内存之间的同步操作,从而保证可见性。
  3. 有序性(Ordering)
    • 程序执行的顺序按照代码的先后顺序执行,不会乱序执行。
    • 有序性通常通过使用 来保证,锁的释放和获取操作可以确保代码的执行顺序。在 Java 中,volatile关键字也可以保证有序性,它通过禁止指令重排序来实现。此外,happens - before原则也可以用来解决有序性问题,它规定了一些操作之间的先后顺序,从而保证程序的正确性。

守护线程

守护线程是为其他线程服务的线程;什么是守护线程?

守护线程(Daemon Thread)是一种特殊的后台线程,它的生命周期完全依赖于用户线程。当所有用户线程结束时,JVM会自动退出,不会等待守护线程执行完毕。

  • 后台服务:为其他线程提供服务
  • 自动终止:主线程结束时自动退出
  • 无需等待:JVM不会等待守护线程完成
  • 典型应用:垃圾回收、监控、日志等

线程优先级

在实际开发中,我们更应该通过合理的线程池配置同步机制 来控制程序流程,而不是依赖这个"不太靠谱"的优先级。什么是线程优先级?

线程优先级是Java中用来影响线程调度顺序 的机制。每个线程都有一个1-10之间的优先级数值,数值越高表示优先级越高。

  • 优先级范围Thread.MIN_PRIORITY(1)Thread.MAX_PRIORITY(10)
  • 默认优先级Thread.NORM_PRIORITY(5)
  • 作用机制 :优先级高的线程更容易被CPU调度执行
  • 重要提醒 :优先级只是建议性的,不保证严格按优先级执行

线程的run()和start()方法的区别

调用方式不同。线程的run()和start()方法有什么区别?

在Java多线程编程中,Thread类的run()start()方法有着本质的区别:

  • start()方法:真正启动一个新线程,使线程进入就绪状态,等待CPU调度执行
  • run()方法:只是普通方法调用,会在当前线程中执行,不会创建新线程

简而言之:**start()**会创建新线程并执行,而 **run()**只是在当前线程中执行一个普通方法 ,在多线程场景下,我们总是应该调用start()而非run()

join()方法

在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。join()方法的作用是什么?

join()方法是Thread类的一个实例方法,核心作用 是让当前线程等待调用join()方法的线程执行完毕后再继续执行。简单来说,如果线程A中调用了线程B的join()方法,那么线程A会被阻塞,直到线程B执行完成后,线程A才能继续执行。

join()方法有三种形式:

  • join()无限等待直到目标线程执行完成
  • join(long millis):最多等待指定毫秒数
  • join(long millis, int nanos):最多等待指定的毫秒数加纳秒数

使用场景:多线程环境下的任务协调,比如主线程需要等待所有子线程完成后再进行结果汇总。

记住,合理使用join()方法可以让多线程代码更具可控性和可预测性,但过度使用会增加线程间的耦合度,使代码难以维护。在实际项目中,要根据具体场景选择最合适的线程协作方式!

interrupt()方法

线程中断:interrupt()方法的作用是什么?

Java中的interrupt()方法是线程间协作的一种机制,不是强制停止线程,而是给线程发送一个"中断信号",告知目标线程"有人希望你停止运行"。

核心要点:

  • interrupt()会将线程的中断状态 设置为true
  • 被中断的线程可以检测 这个状态并决定如何响应
  • 处于wait()/join()/sleep()状态的线程收到中断会抛出InterruptedException异常
  • 中断是一种协作机制,而非强制终止线程的手段

xxl-job中停止线程的一种方式:

java 复制代码
public void toStop() {
    toStop = true;

    /**
     * 安全停止并等待注册线程终止
     * 1. 若线程存在则先发送中断信号
     * 2. 等待目标线程完全终止
     * 3. 捕获并记录线程等待过程中可能发生的异常
     */
    if (registryThread != null) {
        registryThread.interrupt();
        try {
            registryThread.join();
        } catch (Throwable e) {
            logger.error(e.getMessage(), e);
        }
    }
}

sleep()和wait()的区别

让线程暂停,前者是"假死",后者是"真等待"。sleep()和wait()的区别是什么?

sleep()wait() 都能让线程暂停,但它们有本质区别:

  • **sleep()** 📌:属于 Thread 类的静态方法 ,让当前线程休眠指定时间,不会释放锁资源
  • **wait()** 📌:属于 Object 类的实例方法 ,让当前线程等待直到被唤醒,会释放锁资源
对比项 sleep() wait()
所属类 Thread类(静态方法) Object类(实例方法)
锁释放 ❌ 不释放 ✅ 释放
使用场景 简单延时 线程间通信
唤醒方式 时间到自动唤醒 notify()/notifyAll()唤醒

yield()方法

线程暂停。yield()方法的作用是什么?

yield()方法是Thread类的静态方法 ,它的作用是让当前正在执行的线程暂停一下 ,给其他具有相同优先级的线程一个执行机会。

关键特点:

  • 不会释放锁资源 💡
  • 只是从运行状态转为就绪状态
  • 不保证其他线程一定会执行
  • 属于一种"友好的提醒"机制

简单来说,就是告诉线程调度器:"我可以先让一下,但不一定真的让出去"。

yield() vs sleep() vs wait() 对比表 📝

方法 是否释放锁 线程状态变化 何时重新执行 使用场景
yield() ❌ 不释放 运行→就绪 立即可能重新获得CPU 让出CPU给同优先级线程
sleep() ❌ 不释放 运行→阻塞 指定时间后 暂停指定时间
wait() ✅ 释放 运行→阻塞 被notify()唤醒 线程间协作等待

notify()和notifyAll()的区别

不确定就用notifyAll(),宁可多唤醒,不可错过通知notify()和notifyAll()的区别是什么?

notify()notifyAll()都是Object类中的方法,用于线程间通信,但它们存在本质区别:

  • notify() 只唤醒一个 正在等待该对象锁的线程(具体哪一个由JVM决定)
  • notifyAll() 唤醒所有 正在等待该对象锁的线程

在使用时的关键区别是:

  • 使用notify()时,如果多个线程都在等待,被唤醒的线程是随机 的,可能导致死锁问题
  • 使用notifyAll()更加安全 ,但可能造成性能开销,因为所有线程都会被唤醒并竞争锁资源

公平锁和非公平锁

synchronized 关键字是非公平锁。而 ReentrantLock 类可以实现公平锁和非公平锁,默认情况下是非公平锁,可通过构造方法传入参数设置为公平锁。什么是公平锁和非公平锁?

  1. 公平锁
    • 按照线程请求的顺序获取锁
    • 等待时间最长的线程优先获取锁
    • 性能相对较低,但更公平
  2. 非公平锁
    • 允许线程"插队"获取锁
    • 不保证等待线程获取锁的顺序
    • 性能更好,但可能造成饥饿
  3. 使用场景
    • 公平锁:对公平性要求高的场景
    • 非公平锁:注重吞吐量的场景

锁消除和锁粗化

了解即可。什么是锁消除和锁粗化?

  1. 锁消除
    • 定义:JIT编译器在运行时,去除不可能存在竞争的锁
    • 原理:逃逸分析,判断对象是否只被一个线程访问
    • 目的:消除不必要的同步,提高性能
  2. 锁粗化
    • 定义:将临近的多个同步块合并为一个更大的同步块
    • 原理:减少反复加锁解锁的开销
    • 目的:提高性能,减少同步负担

停止线程

中断+标志位。如何正确停止一个线程?

Java中正确停止线程的方式主要有两种:

  1. 使用中断机制(推荐) :通过调用线程的interrupt()方法,设置线程的中断状态标志,然后在线程内部检查中断状态并做出响应。
  2. 使用共享标志位 :在线程中设置一个可见的标志位 (使用volatile修饰),线程定期检查这个标志位决定是否继续执行。

不应使用Thread.stop()Thread.suspend()Thread.resume()方法已被废弃,使用它们可能导致线程不安全问题。

线程死锁

资源相互获取。说一说Java的死锁问题及解决方案

锁是指两个或多个线程互相持有对方需要的资源 ,都在等待对方释放资源,导致这些线程永久阻塞的状态。

死锁产生的四个必要条件:

  • 互斥条件:资源不能被共享,只能被一个线程独占
  • 请求与保持条件:线程已占有资源,又提出新的资源请求
  • 不剥夺条件:线程已获得的资源,在未使用完前不能被强行剥夺
  • 循环等待条件:多个线程形成环路等待资源的情形

解决方案:

  1. 破坏环路等待:按顺序获取锁
  2. 使用显式锁 :如ReentrantLock设置超时
  3. 死锁检测与恢复 :使用jstack等工具检测并解决

🌟什么是CAS?

比较交换的无锁算法。什么是CAS?CAS有什么问题?

CAS的概念与原理 🔄

CAS(Compare And Swap)是一种无锁算法 ,它是CPU原子指令的一种,用于在多线程环境下实现同步操作。CAS操作包含三个操作数:

  • 内存位置V:需要更新的变量
  • 预期原值A:变量预期的值
  • 新值B:将要设置的新值

CAS的核心思想是:先比较,再交换。它会判断当前内存值是否与预期值相同,如果相同则将内存值修改为新值,整个过程是原子性的;如果不同,说明有其他线程修改了这个值,本次CAS操作失败。

CAS的主要问题 ⚠️

  1. ABA问题:如果一个值从A变成B再变回A,CAS会误认为它没有被修改过
  2. 循环时间长开销大:CAS失败时通常会循环重试,这会导致CPU资源浪费
  3. 只能保证一个共享变量的原子操作:不能保证多个变量操作的原子性

CAS在Java中的应用场景 🏢

CAS操作在Java并发包中应用广泛:

  1. 原子类AtomicIntegerAtomicLongAtomicReference等;
  2. 并发工具类CountDownLatchCyclicBarrier等的底层实现;
  3. 并发容器ConcurrentHashMap等的实现机制;

CompletableFuture的使用

Java8的一大利器。CompletableFuture的特点和用法

CompletableFuture是Java 8引入的一个异步编程利器,它是Future接口的扩展和增强。与传统的Future相比,它最大的特点是:

支持链式调用和组合操作,让异步编程变得优雅又高效!

它的核心特点包括:

  • 不需要显式检查完成状态,告别了Future.get()的阻塞困扰
  • 支持回调函数,完成时自动触发下一步操作
  • 提供丰富的组合方式,可以将多个异步任务按需组合
  • 异常处理更灵活,支持类似try-catch的异常处理机制
  • 支持自定义线程池,不再局限于默认的ForkJoinPool

使用时最常见的三类方法:

  1. 创建方法supplyAsync()(有返回值)和runAsync()(无返回值)
  2. 转换方法thenApply()thenAccept()thenRun()
  3. 组合方法thenCompose()thenCombine()allOf()anyOf()

内存模型

内存模型介绍

一种规范。什么是Java内存模型?

Java内存模型(JMM)是一种规范,定义了Java程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。主要目的是解决多线程通信问题,包括可见性、原子性和有序性:

  1. 可见性:一个线程对共享变量的修改,其他线程能够立即看到
  2. 原子性:一个操作或多个操作要么全部执行并且不会被打断,要么就都不执行
  3. 有序性:程序执行的顺序按照代码的先后顺序执行

处理方案

  1. 可见性
    • volatile关键字
    • synchronized关键字
    • final关键字
  2. 原子性
    • synchronized关键字
    • Lock接口
    • Atomic类
  3. 有序性
    • volatile关键字
    • synchronized关键字
    • happens-before原则

主内存和工作内存

内存模型的核心。JMM中的主内存和工作内存是什么?

  1. 主内存(Main Memory)
    • 所有线程共享的内存区域
    • 存储所有变量的实际值
    • 类似于物理内存的概念
  2. 工作内存(Working Memory)
    • 每个线程独有的内存区域
    • 存储该线程需要使用的变量的副本
    • 类似于CPU缓存的概念

内存屏障

阻止重排序+保证可见性。什么是内存屏障?

内存屏障是一种底层同步机制,用来确保指令执行的顺序性内存访问的可见性。简单来说,它就像在代码中插入了一道"栅栏",强制处理器按照你期望的顺序执行指令,防止编译器和CPU对指令进行重排序优化,同时保证多核CPU环境下各个处理器的缓存数据能够同步到主内存,使其对其他处理器可见。

在Java中,我们不直接使用内存屏障,而是通过volatile关键字、synchronized关键字以及各种并发工具类来间接使用它,这些高级同步机制底层都依赖于内存屏障实现。

happens-before规则

先行发生原则。happens-before规则有哪些?.

JMM 为程序中所有的操作定义了一个偏序关系,称之为 先行发生原则(Happens-Before)

Happens-Before 是指 前面一个操作的结果对后续操作是可见的。

Happens-Before 非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作间是否可能存在冲突的所有问题。

  • 程序次序规则 - 一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  • 锁定规则 - 一个 unLock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile 变量规则 - 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
  • 线程启动规则 - Thread 对象的 start() 方法先行发生于此线程的每个一个动作。
  • 线程终止规则 - 线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行。
  • 线程中断规则 - 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到是否有中断发生。
  • 对象终结规则 - 一个对象的初始化完成先行发生于它的 finalize() 方法的开始。
  • 传递性 - 如果操作 A 先行发生于 操作 B,而操作 B 又 先行发生于 操作 C,则可以得出操作 A 先行发生于 操作 C。

重排序

有利于提升性能。什么是重排序?有哪几种类型?

  1. 重排序定义
    • 为了提高性能,编译器和处理器会对指令进行重新排序
    • 在不改变程序执行结果的前提下,对指令执行顺序进行调整
  2. 重排序类型
    • 编译器优化重排序
    • 指令级并行重排序(处理器优化)
    • 内存系统重排序(缓存、写缓冲区导致)
  3. happens-before原则
    • Java内存模型(JMM)通过happens-before规则来确保并发的正确性
    • 规定了哪些写操作对其他读操作是可见的

volatile

🌟volatile关键词

⚠️重要关键词。volatile关键字的作用是什么?

  1. 保证可见性
    • 一个线程修改变量后,其他线程能立即看到最新值
    • 防止指令重排序
  2. 不保证原子性
    • 不能保证复合操作的原子性
    • 适用于单个变量的读写
  3. 保证有序性
    • 在volatile变量操作前后插入内存屏障

volatile的内存屏障

可见性+有序性。volatile的内存屏障是什么?

  1. 四种内存屏障
    • LoadLoad屏障:确保volatile读操作之前的读操作先执行
    • StoreStore屏障:确保volatile写操作之前的写操作先执行
    • LoadStore屏障:确保volatile读操作之前的读操作先执行
    • StoreLoad屏障:确保volatile写操作之前的写操作先执行
  2. 作用
    • 防止指令重排序
    • 保证内存可见性
    • 维护顺序一致性
  3. 实现原则
    • 写操作:StoreStore屏障 + 写操作 + StoreLoad屏障
    • 读操作:LoadLoad屏障 + 读操作 + LoadStore屏障

volatile能保证原子性吗?

不保证原子性。volatile能保证原子性吗?

  1. 不能保证原子性
    • volatile只保证可见性和有序性
    • 不能保证复合操作的原子性
  2. 常见误区
    • i++ 这样的操作不是原子的
    • volatile变量的读写虽然是立即可见的,但复合操作仍然会有问题
  3. 解决方案
    • 使用synchronized关键字
    • 使用AtomicInteger等原子类
    • 使用Lock接口的实现类

volatile和synchronized的区别

关键词比较。volatile和synchronized的区别是什么?

  1. 作用范围不同
    • volatile: 仅能修饰变量
    • synchronized: 可修饰方法、代码块
  2. 原子性
    • volatile: 不保证原子性
    • synchronized: 保证原子性
  3. 可见性
    • volatile: 保证可见性
    • synchronized: 保证可见性
  4. 有序性
    • volatile: 禁止指令重排
    • synchronized: 保证有序性
  5. 性能
    • volatile: 轻量级,性能好
    • synchronized: 重量级,性能较差

synchronized

✨synchronized关键字

synchronized关键字是Java中用于实现线程同步的关键机制,主要作用有:

  1. 原子性:确保被修饰的方法或代码块在同一时间只能被一个线程执行
  2. 可见性:保证共享变量的修改对所有线程可见
  3. 有序性:禁止指令重排,确保代码执行顺序

synchronized可以用于修饰实例方法静态方法代码块,通过获取对象的监视器锁(Monitor)实现互斥访问。

特性 synchronized Lock接口
锁的获取 自动获取、释放 手动调用lock()和unlock()
灵活性 较低 高(支持尝试获取、限时获取)
公平性 非公平锁 支持公平和非公平模式
状态感知 不支持 支持查询锁状态
中断响应 不可中断 支持响应中断
实现难度 简单易用 稍复杂但功能强大

synchronized的锁升级过程

1.6升级关键。synchronized的锁升级过程是什么?

synchronized锁升级过程(锁膨胀过程):

  1. 无锁状态偏向锁轻量级锁重量级锁
  2. 主要特点
    • 锁升级是单向的,不能降级
    • 目的是减少锁竞争带来的性能消耗
    • 针对不同场景采用不同的锁实现
  3. 升级条件
    • 偏向锁:第一次有线程获取锁时
    • 轻量级锁:其他线程竞争偏向锁
    • 重量级锁:轻量级锁自旋超过阈值

AQS

✨AQS的原理

从设计者角度去思考它的原理。AQS(AbstractQueuedSynchronizer)的原理是什么?

推荐视频:面试官内心os:坏了,这家伙当我面设计AQS_哔哩哔哩_bilibili

AQS(AbstractQueuedSynchronizer)是Java并发包中的一个核心框架,它提供了一套通用的机制来实现锁和同步器。简单来说,AQS的原理是:

  • 维护了一个volatile整型状态变量state 和一个FIFO等待队列
  • 通过CAS操作修改state值来表示资源的占用情况
  • 线程请求资源失败时,会被封装成Node节点加入FIFO队列
  • 提供了独占模式共享模式两种资源访问方式
  • 子类通过继承并重写指定方法来实现自己的同步器逻辑

像ReentrantLock、CountDownLatch、Semaphore等并发工具类,都是基于AQS实现的,它是Java并发编程的"地基"。

✨ReentrantLock的特点

重点方法,重点理解!ReentrantLock的特点是什么?

  1. 可重入性
    • 同一线程可以多次获取同一把锁
    • 支持递归调用
  2. 公平性选择
    • 支持公平锁和非公平锁
    • 可在构造时选择是否公平
  3. 灵活性
    • 支持中断等待
    • 可设置超时时间
    • 可以获取等待线程列表

ReentrantLock和synchronized的区别

区别比较。ReentrantLock和synchronized的区别是什么?

  1. 功能差异
    • ReentrantLock提供了更多高级功能(公平锁、可中断、超时等待)
    • synchronized是Java关键字,ReentrantLock是类
  2. 使用方式
    • synchronized自动释放锁
    • ReentrantLock需要手动释放锁(try-finally)
  3. 性能比较
    • JDK1.6后synchronized进行了优化,两者性能接近
    • ReentrantLock在高竞争下有更好的性能表现
特性 synchronized ReentrantLock 选择建议
简单同步 synchronized
超时等待 × ReentrantLock
公平锁 × ReentrantLock
可中断 × ReentrantLock
多条件变量 × ReentrantLock
自动释放锁 × synchronized

CountDownLatch的作用和原理

计数器 --> 用于等待型场景。CountDownLatch的作用和原理?

CountDownLatch是Java并发包中的一个同步辅助类 ,主要用于协调多个线程之间的同步 。它的核心作用是允许一个或多个线程等待其他线程完成操作后再继续执行。

CountDownLatch工作原理基于一个计数器机制

  • 初始化时设定一个正整数作为计数值
  • 每当一个线程完成任务,调用countDown()方法使计数器减1
  • 调用await()方法的线程会被阻塞,直到计数器减为0

这种机制非常适合等待型场景,比如主线程需要等待所有工作线程完成后再进行结果汇总,或者需要等待所有服务都启动完毕才能继续执行业务逻辑。

CyclicBarrier的作用和原理

循环栅栏。CyclicBarrier的作用和原理?

CyclicBarrier是Java并发包中的一个同步辅助类 ,主要用于协调多个线程相互等待 ,直到所有线程都到达一个共同的障碍点(barrier)才继续执行。它的核心特点是可循环使用(Cyclic)。

CyclicBarrier的工作原理基于计数器与等待机制

  • 初始化时设定一个参与线程数量作为计数值
  • 每个线程执行到某个点时调用await()方法,表示已到达屏障点
  • 线程会在屏障点被阻塞,直到所有参与线程都到达屏障点
  • 当最后一个线程到达屏障点时,屏障打开,所有线程继续执行
  • 屏障自动重置,可以被重复使用

这种机制非常适合多线程协作场景,如多阶段计算任务、并行迭代算法等需要分步骤同步的场景。

特性 CyclicBarrier CountDownLatch
重用性 ✅ 可重复使用 ❌ 一次性使用
计数重置 ✅ 自动重置 ❌ 不能重置
触发方式 所有线程必须主动调用await() 任何线程都可以调用countDown()
执行动作 ✅ 可执行自定义的屏障行为 ❌ 没有此功能
线程角色 参与的线程彼此对等 等待线程和被等待线程角色不同
等待形式 多线程相互等待 一个或多个线程等待其他线程
底层实现 ReentrantLock和Condition AbstractQueuedSynchronizer(AQS)

ThreadLocal

ThreadLocal的作用和原理

线程数据隔离。ThreadLocal的作用和原理

ThreadLocal是Java提供的一个线程本地变量工具类,它的主要作用是提供线程隔离的变量,让每个线程都拥有自己独立的变量副本。

ThreadLocal的核心原理是:

  • 每个Thread对象内部维护了一个ThreadLocalMap类型的成员变量
  • 这个Map的键是ThreadLocal对象本身 ,值是存储的线程私有数据
  • 当访问ThreadLocal变量时,实际上是从当前线程的ThreadLocalMap中获取数据
  • 不同线程访问同一个ThreadLocal对象时,它们操作的是各自线程私有的数据副本,互不干扰

ThreadLocal解决了线程安全问题 的同时,避免了synchronized带来的性能开销 ,适用于需要线程隔离且贯穿线程生命周期的数据场景。

特性 ThreadLocal synchronized
目的 线程数据隔离 线程数据共享但同步访问
性能 较高 较低(有锁竞争开销)
应用场景 线程隔离的数据副本 多线程共享数据
实现方式 每个线程独立副本 同步互斥访问

ThreadLocal的内存泄漏问题

记得显式清除。ThreadLocal的内存泄漏问题

ThreadLocal存在内存泄漏风险主要是因为其实现机制。ThreadLocal变量与Thread生命周期绑定,每个线程都维护一个ThreadLocalMap ,它以ThreadLocal对象 ,存储的值为线程本地变量

ThreadLocalMap使用的是ThreadLocal的弱引用作为Key,这就导致了潜在的内存泄漏问题:当ThreadLocal对象被回收后,ThreadLocalMap中的Entry key变为null,但value仍然存在强引用,如果线程长期存活(如线程池中的线程),这部分内存就永远无法被回收。

解决方案 :👉 使用完ThreadLocal后,显式调用remove()方法清除数据。

引用类型 ThreadLocalMap中的key 外部的ThreadLocal变量 可能导致的问题
强引用 不会被回收 不会被回收 ThreadLocal不会被回收,可能造成内存泄漏
弱引用 可被GC回收 可被GC回收 ThreadLocal被回收后,value无法被访问,造成内存泄漏

线程池

🌟为什么要使用线程池

并发必问题!为什么要使用线程池?

在Java并发编程中,使用线程池主要有四大优势:

  1. **线程复用**:避免了频繁创建和销毁线程的开销,提高了系统的响应速度和性能。
  2. **控制并发数量**:可以根据系统资源情况,合理设置线程数量上限,防止资源耗尽。
  3. **统一任务管理**:提供任务队列机制,实现任务的排队执行,平滑处理负载峰值。
  4. **提供扩展功能**:支持定时执行、任务取消、异常处理等高级特性,大大简化了并发编程。

线程池的工作原理是什么

核心-队列-最大-拒绝线程池的工作原理是什么?

线程池的工作原理可以概括为"核心-队列-最大-拒绝"的处理流程:

  1. 当有新任务提交时,如果线程数小于核心线程数,创建新线程处理
  2. 如果线程数已达到核心线程数,将任务放入任务队列
  3. 如果队列已满但未达到最大线程数,创建新线程处理
  4. 如果队列已满且线程数达到最大值,执行拒绝策略

Java中主要通过ThreadPoolExecutor实现,通过合理设置corePoolSizemaximumPoolSizekeepAliveTimeworkQueueRejectedExecutionHandler等参数来优化性能。

如何正确配置线程池参数?

动态调整参数。如何正确配置线程池参数?

在Java并发编程中,正确配置线程池参数是提高应用性能的关键。配置线程池主要考虑以下几个核心参数:

  1. 核心线程数(corePoolSize) : 根据CPU密集型IO密集型任务特点决定
    • CPU密集型: 核心线程数 = CPU核心数 + 1
    • IO密集型: 核心线程数 = CPU核心数 * (1 + 等待时间/计算时间)
  2. 最大线程数(maximumPoolSize) : 通常设置为核心线程数的2-3倍,需根据系统资源和并发量评估
  3. 队列类型与容量 : 选择合适的工作队列类型(ArrayBlockingQueueLinkedBlockingQueue等)和合理容量
  4. 拒绝策略: 根据业务需求选择合适的拒绝处理策略
  5. 线程存活时间(keepAliveTime): 非核心线程的空闲存活时间,通常几十秒即可

关键点是:参数配置没有万能公式,需要结合具体业务场景、系统资源和实际压测结果进行调优。

线程池的种类有哪些

推荐手动创建。线程池的种类有哪些?

Java中的线程池主要有以下几种类型:

  1. **FixedThreadPool** - 固定大小的线程池,核心线程数等于最大线程数,任务队列无限大
  2. **CachedThreadPool** - 可缓存的线程池,按需创建线程,空闲线程会被回收
  3. **SingleThreadExecutor** - 单线程的线程池,只有一个工作线程处理任务队列
  4. **ScheduledThreadPool** - 定时任务线程池,支持定时或周期性执行任务
  5. **WorkStealingPool** (Java 8新增) - 工作窃取线程池,基于ForkJoinPool实现
  6. **ForkJoinPool** - 分治算法线程池,适合将大任务拆分成小任务并行处理

这些线程池都由Executors工厂类提供创建方法,但在生产环境中,更推荐手动创建 **ThreadPoolExecutor**自定义线程池,以避免资源耗尽风险。

线程池的核心参数有哪些

核心-队列-最大-拒绝线程池的核心参数有哪些?

Java线程池主要有7个核心参数,面试中一定要完整记住:

  1. corePoolSize:核心线程数,线程池中常驻的线程数量
  2. maximumPoolSize:最大线程数,线程池能容纳的最大线程数
  3. keepAliveTime:线程空闲时间,非核心线程空闲超过这个时间会被回收
  4. unit:时间单位,配合keepAliveTime使用
  5. workQueue:工作队列,存放等待执行的任务
  6. threadFactory:线程工厂,用于创建新线程
  7. handler:拒绝策略,当队列和线程池都满了时如何处理新任务

回答技巧:先简要概括"7个参数",然后按照任务执行顺序(核心线程→队列→最大线程→拒绝策略)来说明每个参数的作用,展示你对线程池运行机制的理解。

线程池的异常处理机制

主动补获。线程池的异常处理机制是什么?

在Java线程池中,异常处理机制主要有以下几种方式:

  1. 默认情况下 ,如果任务抛出了未捕获的异常,线程池会:
    • 将这个异常打印到控制台
    • 让这个工作线程死亡
    • 创建一个新的线程替代它
    • 不会通知调用者
  2. 主动捕获异常 的方法:
    • 在提交的任务中使用 try-catch
    • 使用 UncaughtExceptionHandler 全局处理
    • 通过 Future 对象的 get() 方法捕获
  3. 对于不同提交方式 的异常处理:
    • execute():异常直接抛出到控制台
    • submit():异常被包装在 Future 中,调用 get() 时才会抛出。

实际开发中,可以这样设计线程池的异常处理:

  1. 监控为先 :设置 UncaughtExceptionHandler 确保所有异常都被记录
  2. 降级机制:提供合理的降级或重试策略
  3. 隔离原则:确保一个任务的异常不会影响其他任务
  4. 细粒度控制:根据业务重要性区分不同处理方式

参考

室友打了一把王者就学会了 Java 多线程

Java并发简介 | JAVACORE

线程基础的全景图:Java开发者必须掌握的核心知识点

Java并发常见面试题总结(上)

Java并发编程面试题

相关推荐
夜晚回家26 分钟前
「Java教案」Java程序的构成
java·开发语言
全栈凯哥37 分钟前
领域驱动设计 (Domain-Driven Design, DDD)
java
酱学编程39 分钟前
【监控】Spring Boot 应用监控
java·spring boot·后端·prometheus
小陈又菜1 小时前
Real SQL Programming
数据库·sql
%d%d21 小时前
Redis 插入中文乱码键
数据库·redis·缓存
小阳拱白菜1 小时前
intell JIDEAL的快捷键
java
匆匆整棹还1 小时前
idea配置android--以idea2023为例
android·java·intellij-idea
goldfishsky1 小时前
elasticsearch
开发语言·数据库·python
梦想实现家_Z1 小时前
拆解Java MCP Server SSE代码
java·spring·mcp
梦想实现家_Z1 小时前
原生Java SDK实现MCP Server(基于WebMvc的SSE通信方式)
java·spring·mcp