面试篇:Java并发与多线程

news2025/4/24 8:08:01

基础概念

什么是线程?线程和进程的区别是什么?

线程 是程序执行的最小单位,它是 CPU 调度和执行的基本单元。一个进程可以包含多个线程,这些线程共享进程的资源(如内存),但每个线程有自己的栈和程序计数器。

线程和进程的区别

  1. 定义

    • 进程 是正在执行的程序实例,它是资源分配的基本单位,拥有自己的独立地址空间、内存、数据栈等。

    • 线程 是进程中的一个执行单元,多个线程共享进程的资源(内存、文件句柄等)。

  2. 资源

    • 进程 拥有独立的内存空间和系统资源,每个进程有自己的堆和数据段。

    • 线程 在同一进程内共享内存空间,因此线程之间的通信比进程间通信更高效。

  3. 创建和销毁

    • 进程 的创建和销毁开销大,因为操作系统需要为每个进程分配独立的内存空间。

    • 线程 的创建和销毁相对轻量,开销较小,因为线程共享同一进程的资源。

  4. 通信

    • 进程间通信(IPC) 较为复杂,通常需要通过管道、消息队列、共享内存等方式。

    • 线程间通信 相对简单,因为同一进程中的线程共享内存,可以通过共享变量进行通信。

  5. 调度

    • 进程 的切换需要较多的系统资源,因为每个进程都有独立的内存空间和资源。

    • 线程 的切换较为轻便,调度和上下文切换的成本较低。

总结:进程 是资源分配的基本单位,线程是程序执行的基本单位。线程更轻量,多个线程共享进程资源,但进程之间相互独立。

如何创建线程?有几种方式?

创建线程有两种主要方式:

  1. 继承 Thread 类:通过继承 Thread 类并重写 run() 方法来定义线程的执行内容,最后调用 start() 方法启动线程。

  2. 实现 Runnable 接口:通过实现 Runnable 接口的 run() 方法来定义线程执行的任务,然后将 Runnable 实例传递给 Thread 类的构造函数,并调用 start() 启动线程。

这两种方式的区别在于,继承 Thread 类时只能继承一个类,而实现 Runnable 接口更灵活,因为一个类可以实现多个接口,适合更复杂的应用场景。

Runnable 和 Callable 的区别?

RunnableCallable 都是用于定义线程任务的接口,但它们有一些关键的区别:

  1. 返回值

    • Runnable 接口的 run() 方法没有返回值,也无法抛出异常。

    • Callable 接口的 call() 方法有返回值,能够返回一个结果或者抛出异常。

  2. 异常处理

    • Runnablerun() 方法无法抛出任何检查型异常(checked exceptions),只能抛出运行时异常(unchecked exceptions)。

    • Callablecall() 方法可以抛出任何类型的异常(包括检查型异常)。

  3. 执行方式

    • Runnable 是传统的线程任务接口,适用于没有返回结果或不需要处理异常的简单任务。

    • Callable 更灵活,适用于需要返回结果或处理异常的任务,通常与 ExecutorService 一起使用,可以获取任务的返回值。

  4. Future 的结合

    • Runnable 任务无法直接获取结果,因此不能与 Future 结合使用来获取任务的返回值。

    • Callable 返回值类型为 V,可以与 ExecutorService.submit() 方法一起使用,返回一个 Future 对象,通过 Future.get() 可以获取任务执行的结果或捕获异常。

总结:如果你需要线程执行结果或可能抛出异常,使用 Callable;如果只是需要执行某些操作而不关心返回值或异常,Runnable 更为简单和直接。

线程的生命周期(状态)有哪些?

线程的生命周期包括以下几种状态:

  1. 新建(New) 线程在创建后,尚未调用 start() 方法时,处于 新建状态。此时线程对象已经创建,但尚未开始执行。

  2. 就绪(Runnable) 线程调用 start() 方法后,线程进入 就绪状态,此时线程已经准备好执行,但尚未获取到 CPU 资源。操作系统的线程调度器会根据某些算法决定哪个线程获得 CPU 执行权。

  3. 运行(Running) 当线程获得 CPU 时间片后,它进入 运行状态,并开始执行 run() 方法中的代码。此时线程正在执行任务。

  4. 阻塞(Blocked) 线程在等待某些资源(如 I/O 操作、锁资源等)时,会进入 阻塞状态。线程无法继续执行,直到等待的资源可用。

  5. 等待(Waiting) 线程进入 等待状态,通常是因为调用了 wait()join()park() 方法,线程会一直等待直到被其他线程唤醒(如调用 notify()notifyAll()interrupt() 等)。

  6. 超时等待(Timed Waiting) 线程进入 超时等待状态,当线程调用 sleep()join(long millis)wait(long millis) 等带有超时参数的方法时,线程会在指定的时间内处于等待状态。如果超时未被唤醒,线程会自动回到 就绪状态

  7. 终止(Terminated) 线程的 run() 方法执行完毕,或者线程因异常退出时,线程进入 终止状态。此时线程生命周期结束,无法重新启动。

总结:线程的生命周期涉及从新建到终止的多个状态,线程可以在不同状态之间切换,具体状态由线程调度器和程序的运行条件决定。

sleep() 和 wait() 的区别?

sleep()wait() 都是让当前线程暂停执行,但它们有几个关键的区别:

  1. 作用对象

    • sleep()Thread 类的方法,它使当前线程暂停执行指定的时间,当前线程仍然占有 CPU 资源。它不需要持有锁。

    • wait()Object 类的方法,它使当前线程暂停执行,并且释放对象的锁,直到线程被其他线程唤醒。wait() 只能在同步块或同步方法中使用。

  2. 暂停时间

    • sleep() 使线程暂停指定的时间(毫秒和纳秒),一旦时间到了,线程会自动回到就绪状态,等待操作系统重新调度。

    • wait() 会让线程一直进入等待状态,直到其他线程通过 notify()notifyAll() 唤醒它,或者超时(如果指定了超时时间)。

  3. 线程的锁状态

    • sleep() 线程暂停时 不会释放锁,线程仍持有它在进入 sleep() 前获得的锁。

    • wait() 线程暂停时 会释放锁,线程放弃当前对象的锁,其他线程可以获得该锁,执行操作。

  4. 异常处理

    • sleep() 会抛出 InterruptedException,如果在 sleep() 期间线程被中断,会抛出此异常。

    • wait() 也会抛出 InterruptedException,如果在等待过程中线程被中断,会抛出此异常。

  5. 常见使用场景

    • sleep() 通常用于让线程暂停指定的时间,可以用于周期性的任务、定时器等场景。

    • wait() 通常用于线程间的通信和协作,常见于生产者-消费者模型,线程需要等待某个条件成立或等待资源释放时使用。

总结:sleep() 是一个用于让当前线程暂停执行的简单方法,适用于线程的定时任务;而 wait() 主要用于线程间的协作与通信,常用于多线程同步中。

yield() 和 join() 的作用是什么?

yield()join() 都是用于线程控制的静态方法,但它们的作用和使用场景有所不同:

  1. yield()

    • 作用Thread.yield() 方法用于 提示线程调度器 当前线程愿意让出 CPU 执行时间片,允许其他同优先级的线程执行。调用 yield() 并不会让当前线程停止执行,只是使当前线程回到就绪状态,调度器会选择其他同优先级的线程执行。

    • 特点

      • 线程仍然处于就绪状态,调用 yield() 后,线程不会立即停止,而是根据调度器的策略,可能会被调度出去,其他线程有机会执行。

      • 如果没有其他同优先级的线程准备好执行,当前线程可能继续执行。

      • yield() 的调用效果依赖于操作系统和 JVM 的调度策略,并非一定会让出 CPU。

    • 常见场景:通常用于调度和协调线程的执行,特别是在多线程程序中希望让其他线程有机会执行时。

  2. join()

    • 作用Thread.join() 方法使得 当前线程等待另一个线程执行完毕 后再继续执行。即当前线程会被阻塞,直到调用 join() 的线程执行完 run() 方法。

    • 特点

      • 如果线程 A 调用了线程 B 的 join() 方法,线程 A 会被阻塞,直到线程 B 执行完毕。

      • join() 可以指定一个超时时间,即 join(long millis),如果超时,线程 A 会继续执行。

      • join() 方法常用于线程的协调,确保一个线程在另一个线程执行完成之后再继续执行。

    • 常见场景:通常用于在多线程程序中,等待某个线程完成任务后再执行后续的操作。例如,在多个线程并行执行时,主线程等待所有子线程完成后再进行下一步操作。

总结:

  • yield() 是一种提示线程调度器让当前线程暂停执行,可能会导致当前线程被暂时挂起,等待其他线程执行。

  • join() 是一种同步机制,用于确保当前线程在另一个线程执行完成后再继续执行。

线程安全与同步

什么是线程安全?如何保证线程安全?

线程安全指的是在多线程环境下,多个线程对共享资源进行操作时,能够保证数据的一致性和正确性,不会出现竞态条件或数据错误。

如何保证线程安全?

  1. 使用同步(synchronized):通过对共享资源加锁,确保同一时间只有一个线程可以访问被锁住的代码块或方法。

  2. 使用显式锁(如 ReentrantLock:通过 ReentrantLock 提供比 synchronized 更加灵活的锁控制,如可以中断的锁或定时锁。

  3. 原子操作:使用 Atomic 类(如 AtomicInteger)提供原子性操作,无需显式加锁。

  4. 线程局部变量(ThreadLocal):为每个线程提供独立的变量副本,避免共享数据。

  5. 不可变对象:使用不可变对象(如 String),因为它们的状态在创建后不能被改变,从而避免线程安全问题。

总结:确保线程安全的方式主要有同步机制、显式锁、原子操作、线程局部变量等,具体方法根据不同场景选择。

synchronized 关键字的用法?

synchronized 的用法有三种:

  1. 修饰实例方法,锁的是当前对象 this,保证同一时刻只有一个线程执行这个方法。

  2. 修饰静态方法,锁的是当前类的 Class 对象,适用于多个线程访问类级别的资源。

  3. 修饰代码块,锁的是代码块中指定的对象,可以更细粒度地控制同步范围,提升性能。

目的就是让多个线程在访问共享资源时保持互斥,避免并发问题。

synchronized 和 ReentrantLock 的区别?

synchronizedReentrantLock 都可以实现线程同步,但它们有以下主要区别:

  1. 锁的可重入性 两者都是可重入锁,线程可以多次获得同一个锁,不会死锁。

  2. 是否可中断 synchronized 不可中断,线程一旦阻塞,只能等着。 ReentrantLock 可以中断,通过 lockInterruptibly() 方法实现。

  3. 是否可超时 synchronized 无法设置超时时间。 ReentrantLock 可以通过 tryLock(long time) 设置等待锁的时间,超时不再等待。

  4. 公平性 synchronized 是非公平的,谁先抢到谁先执行,不保证顺序。 ReentrantLock 可以设置为公平锁,按线程请求顺序获取锁。

  5. 锁的释放方式 synchronized 是自动释放,方法或代码块执行完自动释放锁。 ReentrantLock 需要手动释放,必须调用 unlock(),否则容易造成死锁。

  6. 灵活性 ReentrantLock 提供更多高级功能,如条件锁(Condition),可实现更复杂的线程协作;synchronized 只能使用 wait()notify()

总结:简单场景用 synchronized 就够了;需要更强控制力、响应中断、可设置超时、需要公平性时,用 ReentrantLock 更合适。

volatile 关键字的作用?

volatile 是一个轻量级的同步机制,用于保证 可见性禁止指令重排序

主要作用有两个:

  1. 可见性:当一个线程修改了被 volatile 修饰的变量,其他线程能立即看到这个修改。它确保了变量从主内存直接读写,而不是使用线程本地缓存。

  2. 禁止指令重排序volatile 会在写操作前插入内存屏障,防止编译器或 CPU 对指令重排,从而保证变量修改的顺序性。

但注意:

  • volatile 不能保证原子性,即多个线程同时修改一个变量时不能保证正确性。比如 count++ 不是原子操作,用 volatile 也无法保证线程安全。

  • 一般用于状态标志、双重检查锁等场景。

总结:volatile 适用于保证一个变量在多线程之间的可见性,但不能代替锁来实现复杂的同步逻辑。

什么是 CAS(Compare-And-Swap)?

CAS,全称是 Compare-And-Swap(比较并交换),是一种原子操作,用于实现无锁并发。

它的核心思想是:比较内存中的值是否和预期值相等,如果相等则更新为新值,否则什么都不做。

执行过程包括三个参数:

  • 内存位置(变量的地址)

  • 预期值(希望内存中的值是这个)

  • 新值(如果相等则把它写进去)

只有当变量的当前值等于预期值时,才会被替换成新值,否则就不做任何操作,通常会进行自旋重试。

CAS 的优点:

  • 是一种乐观锁机制,不需要加锁,因此效率高。

  • 广泛应用于 java.util.concurrent.atomic 包中的原子类,如 AtomicInteger

缺点:

  1. 自旋开销:高并发下可能长时间重试,浪费 CPU。

  2. 只能保证一个变量的原子性,多个变量操作需要配合其他手段。

  3. ABA 问题:如果变量从 A 变成 B 又变回 A,CAS 无法识别这种变化。

为了解决 ABA 问题,可以使用 AtomicStampedReference 这类带版本号的原子类。

Atomic 原子类的作用?

Atomic 原子类的作用是提供一套无锁的线程安全操作,用于保证单个变量在并发环境下的原子性操作,避免使用 synchronized 或显式锁带来的性能开销。

这些类位于 java.util.concurrent.atomic 包中,底层依赖于 CAS(Compare-And-Swap)机制 实现。

常见的原子类包括:

  1. AtomicInteger / AtomicLong / AtomicBoolean 对基本类型进行原子操作,如自增、自减、比较更新等。

  2. AtomicReference 对引用类型进行原子更新,常用于实现非阻塞数据结构或共享对象更新。

  3. AtomicStampedReference 解决 ABA 问题,为引用加上版本号或时间戳。

  4. AtomicMarkableReference 用布尔标记解决类似 ABA 问题,更轻量。

  5. AtomicIntegerArray / AtomicLongArray / AtomicReferenceArray 用于原子地操作数组中的元素。

作用总结:

  • 保证数据的可见性和原子性

  • 避免加锁,提升并发性能

  • 适用于高频简单的并发更新操作,如计数器、状态标志等

但注意:原子类适用于单变量的并发场景,复杂逻辑仍建议使用锁控制。

线程池

为什么要使用线程池?

使用线程池的主要原因是为了更高效、更稳定地管理线程资源。具体来说,有以下几点:

  1. 降低资源开销 每次创建和销毁线程是昂贵的,线程池通过复用线程,减少了频繁创建销毁的成本。

  2. 提高响应速度 任务到来时可以直接复用已有线程,无需等待新线程创建,提高响应效率。

  3. 统一管理线程数量 可以控制并发线程的总量,防止系统因为线程过多而耗尽资源。

  4. 支持任务调度与拒绝策略 线程池支持任务排队、定时执行、周期执行,并可以设置拒绝策略应对超负荷。

  5. 更强的可维护性和可监控性 统一的线程管理便于日志跟踪、异常捕获和资源释放,提高系统稳定性。

总结一句话:线程池是为了让线程的使用更加高效、可控、可维护。

ThreadPoolExecutor 的核心参数有哪些?

ThreadPoolExecutor 是 Java 中最灵活的线程池实现类,它的构造函数包含几个核心参数,决定了线程池的行为和性能。主要参数如下:

  1. corePoolSize(核心线程数) 核心线程会一直保留(除非设置了 allowCoreThreadTimeOut),即使处于空闲状态也不会被回收。线程数小于该值时,新任务会直接创建线程执行。

  2. maximumPoolSize(最大线程数) 线程池能容纳的最大线程数。当任务数过多,阻塞队列满了且核心线程数已满,才会尝试创建非核心线程,直到达到最大线程数。

  3. keepAliveTime(空闲线程存活时间) 非核心线程在空闲状态下,超过该时间会被回收。可以通过设置 allowCoreThreadTimeOut 为 true,让核心线程也受到这个限制。

  4. unit(时间单位) 配合 keepAliveTime 使用,指定时间的单位,例如 TimeUnit.SECONDS。

  5. workQueue(任务队列) 存放等待执行任务的队列,有多种实现,如:

    • LinkedBlockingQueue(无界,常用于执行大量短期任务)

    • ArrayBlockingQueue(有界,常用于控制内存)

    • SynchronousQueue(不存储任务,适用于任务很快就能执行)

  6. threadFactory(线程工厂) 用于创建新线程,通常用来自定义线程名字或设置为守护线程。

  7. handler(拒绝策略) 当线程池已满且队列也满了,任务无法处理时的策略,常见有:

    • AbortPolicy(默认,抛出异常)

    • CallerRunsPolicy(由调用者线程执行)

    • DiscardPolicy(直接丢弃)

    • DiscardOldestPolicy(丢弃最旧的任务)

这些参数的组合决定了线程池的行为模型,合理配置可以提升并发性能,避免线程资源浪费或过载崩溃。

线程池的拒绝策略有哪些?

线程池的拒绝策略定义了在线程数达到 maximumPoolSize 且队列已满时,如何处理新提交的任务。JDK 提供了四种默认策略,都实现了 RejectedExecutionHandler 接口:

  1. AbortPolicy(默认) 直接抛出 RejectedExecutionException 异常,阻止系统继续提交任务,让调用者知道线程池已无法处理新任务。

  2. CallerRunsPolicy 由调用线程(提交任务的线程)来执行任务,不抛异常,避免任务丢失,但可能拖慢主线程速度。

  3. DiscardPolicy 直接丢弃任务,不抛异常,适合对部分任务容忍丢失的场景。

  4. DiscardOldestPolicy 丢弃队列中最旧的任务,然后尝试重新提交当前任务。如果任务提交速率过高,可能导致重要任务被踢出。

除了这四种,也可以自定义拒绝策略,实现 RejectedExecutionHandler 接口,根据业务需求编写逻辑,比如记录日志、发送告警等。

选择哪种策略,需要根据业务容忍度、任务重要性和系统设计来权衡。

Executors 提供的几种线程池?各有什么特点?

Executors 是 Java 提供的线程池工厂类,用于快速创建几种常用线程池,虽然方便,但不推荐在生产中直接使用(理由稍后说)。它提供以下几种线程池:

  1. newFixedThreadPool(int nThreads) 创建一个固定大小的线程池,核心线程数 = 最大线程数,使用 LinkedBlockingQueue(无界队列)。 特点:线程数固定,适合负载稳定、线程数可控的场景。 缺点:任务过多可能导致内存撑爆(因为队列是无界的)。

  2. newCachedThreadPool() 创建一个弹性线程池,线程数几乎不受限,空闲线程60秒回收,使用 SynchronousQueue(不缓存任务)。 特点:适合执行大量短期异步任务。 缺点:高并发下容易创建过多线程,可能导致 OOM。

  3. newSingleThreadExecutor() 创建一个单线程线程池,只有一个核心线程,使用 LinkedBlockingQueue特点:所有任务串行执行,保证顺序性,适合场景如日志写入。 缺点:单线程一旦挂掉,所有任务都不能执行。

  4. newScheduledThreadPool(int corePoolSize) 创建一个支持定时和周期性任务的线程池,类似定时器功能,使用 DelayedWorkQueue特点:适用于定时任务、周期调度任务。 缺点:底层线程数不受限制,仍有内存风险。


为什么不推荐直接使用 Executors? 它们默认的队列和线程数量配置不合理(如无界队列、无限线程),在高并发下容易造成 OOM 或线程堆积。推荐自己使用 ThreadPoolExecutor 显式设置参数,更安全、可控。阿里 Java 开发手册也强烈建议这一点。

线程池的工作流程是怎样的?

线程池的工作流程大致如下:

  1. 提交任务 线程池的工作从调用 execute()submit() 提交任务开始。任务被封装成一个 RunnableCallable 对象,然后被提交到线程池。

  2. 任务排队 任务提交后,首先进入线程池的任务队列(如 BlockingQueue)。线程池会根据队列的类型和大小决定任务是否能够立即执行。常见的队列类型有:

    • 无界队列:任务一直可以加入队列,不会抛出异常(例如 LinkedBlockingQueue)。

    • 有界队列:当队列满时,任务会被拒绝(例如 ArrayBlockingQueue)。

    • 同步队列:每个任务都会直接交给一个线程执行,不会被缓存(例如 SynchronousQueue)。

  3. 线程池工作线程执行任务 如果线程池中有空闲线程,它们会从队列中取出任务并执行。如果没有空闲线程:

    • 如果线程池中的线程数小于核心线程数(corePoolSize),线程池会创建一个新线程来执行任务。

    • 如果线程池中的线程数达到核心线程数,任务将被放入队列,等待已有线程空闲。

    • 如果队列已满,且线程池中的线程数小于最大线程数(maximumPoolSize),线程池会创建新线程处理任务。

    • 如果线程池中的线程数已经达到最大线程数,且队列也已满,线程池会根据设定的拒绝策略(如 AbortPolicy, CallerRunsPolicy 等)处理任务。

  4. 任务完成和线程回收 当线程池中的线程执行完任务后,会进行线程回收:

    • 如果是非核心线程且空闲时间超过了 keepAliveTime,它们会被回收,线程池的线程数会减少。

    • 如果线程池的大小大于 corePoolSize,并且有空闲线程,这些线程会被销毁,直到线程池的线程数等于核心线程数。

  5. 关闭线程池 当线程池的任务执行完毕或不再需要时,调用 shutdown()shutdownNow() 方法来关闭线程池。shutdown() 会等待任务执行完毕后再关闭,而 shutdownNow() 会尝试停止正在执行的任务并返回未执行的任务。


总结: 线程池的工作流程是:

  • 提交任务 → 判断线程池状态(空闲线程/核心线程/最大线程数) → 执行任务或排队等待 → 任务完成 → 线程回收。 线程池通过合理的资源管理和任务调度,优化了线程的创建、复用和销毁,提升了系统的并发处理能力和性能。

并发工具类

CountDownLatch 和 CyclicBarrier 的区别?

CountDownLatchCyclicBarrier 都是 Java 中常用的同步工具类,虽然它们都用于协调多个线程的执行,但它们的工作原理和应用场景有所不同。具体区别如下:

1. 功能和目的

  • CountDownLatch:用于使一个或多个线程等待其他线程完成某些操作后再执行。通过调用 countDown() 来减少计数器,计数器为零时,所有等待的线程可以继续执行。

  • CyclicBarrier:用于使一组线程互相等待,直到所有线程都达到某个屏障点后再继续执行。线程通过调用 await() 来等待,直到所有线程都调用 await()

2. 计数器的可重用性

  • CountDownLatch:计数器在达到零后无法重置,一旦计数器减为零,CountDownLatch 就会结束,不能再重新使用。适用于一次性事件的同步,例如等待所有线程完成某些初始化工作后再继续。

  • CyclicBarrier:计数器在所有线程都到达屏障点后会自动重置,可以重复使用。适用于需要多个阶段、多个线程周期性等待的场景,比如多阶段的并行计算。

3. 使用场景

  • CountDownLatch:典型的场景是等待多个线程执行完某些任务后再继续执行,如主线程等待多个工作线程的完成。

  • CyclicBarrier:典型的场景是多个线程并行执行某些任务,并在完成一个阶段后等待其他线程到达同一阶段,例如多个计算任务在同一屏障点等待,确保同步执行。

4. 计数器操作

  • CountDownLatch:使用 countDown() 方法来减少计数器的值,直到值为零,线程才能继续执行。await() 方法用于等待计数器为零时的通知。

  • CyclicBarrier:使用 await() 方法使线程阻塞,直到所有线程都调用 await(),计数器达到设定值后,所有线程才会被唤醒并继续执行。

5. 线程等待方式

  • CountDownLatch:只能等待,无法重置,所有线程必须在计数器为零后才能继续。

  • CyclicBarrier:线程在到达屏障点后会等待,所有线程都到达后,才会继续执行,可以进行多次等待。

总结

  • CountDownLatch 用于等待某个事件的发生,适用于一次性等待场景。

  • CyclicBarrier 用于同步多线程在某个共同点上进行等待,适用于多阶段的同步场景,且具有可重用性。

Semaphore 的作用是什么?

Semaphore 是 Java 中的一个并发控制工具类,主要用于控制访问某个资源的线程数量。它可以用来限制同时访问某些资源的线程数,避免系统因为过多线程访问共享资源而产生问题(如资源竞争、性能下降、线程安全问题等)。

主要作用:

  1. 限制资源访问数量 Semaphore 通过维护一个计数器来控制同时访问某个资源的线程数量。计数器的初始值代表可以同时访问资源的最大线程数。如果有线程请求访问资源,计数器的值减一;如果计数器值为零,新的线程就会被阻塞,直到有线程释放资源(通过 release() 方法)。

  2. 实现并发控制 它用于实现类似“并发限制”的场景,如限制线程池的并发线程数、数据库连接池、限制访问某个共享资源的线程数量等。

  3. 避免线程过度竞争 在某些场景下,可以用 Semaphore 限制线程访问某些资源,避免资源过度竞争导致的性能瓶颈或者线程崩溃。

核心方法:

  • acquire():获取许可(即请求资源)。如果没有足够的许可,线程会被阻塞,直到有许可可用。

  • release():释放许可,增加计数器的值,允许其他线程访问资源。

  • availablePermits():返回当前可用的许可数量。

  • drainPermits():返回并清空当前所有可用的许可数量。

使用场景:

  1. 控制并发数:例如限制数据库连接池中最大连接数、控制请求访问频率等。

  2. 并发限流:当资源访问过于频繁时,可以使用 Semaphore 来限制并发数,保护资源。

  3. 异步任务管理:可以通过 Semaphore 来限制同时执行的异步任务数量,防止系统过载。

总结:

Semaphore 主要用于控制并发访问的数量,可以限制对共享资源的访问。它通过获取和释放许可来实现线程间的同步与资源控制。

ThreadLocal 的原理和使用场景?

原理

ThreadLocal 是一个线程局部变量类,每个线程都会有自己的变量副本,线程之间不会共享这些副本。它通过每个线程内部维护一个 ThreadLocalMap 来实现这一点,ThreadLocalMapThreadLocal 作为键,线程对应的变量作为值存储。当调用 get()set() 时,实际上操作的是当前线程的局部变量副本,而不是共享变量。

使用场景

  1. 避免共享数据的线程安全问题:当多个线程需要使用相同类型的数据时,使用 ThreadLocal 可以为每个线程提供独立的副本,避免了线程间的共享问题和同步开销。

  2. 性能优化:对于一些频繁访问的资源(如数据库连接、日期格式化等),使用 ThreadLocal 可以避免多线程的同步冲突,提升性能。

  3. 线程独立存储数据:在 Web 开发中,可以用 ThreadLocal 存储线程局部数据,比如每个请求的会话信息,避免不同请求间的数据共享。

简而言之,ThreadLocal 用于确保每个线程都可以拥有自己独立的存储空间,适合处理线程独立数据的场景。

Fork/Join 框架的作用?

Fork/Join 框架是 Java 7 引入的一个并行计算框架,旨在简化并行任务的处理,特别是对于可以被分解成小任务并且最终结果可以合并的计算。其核心思想是分治法(Divide and Conquer),将大任务拆分为多个小任务,分别执行后再合并结果。

作用

  1. 并行化任务 Fork/Join 框架可以通过拆分大任务为多个小任务,并将小任务并行执行,从而有效地利用多核处理器,提高计算效率。

  2. 任务分解与合并 它适合用来处理那些能够递归分解为小子问题的问题。每个子任务完成后,将其结果汇总或合并为最终结果。

  3. 任务调度优化 Fork/Join 框架通过使用工作窃取算法(Work Stealing)来优化任务调度。空闲的工作线程可以窃取其他线程未完成的任务,从而提高系统的吞吐量和资源利用率。

主要组件

  1. ForkJoinPool ForkJoinPoolFork/Join 框架的核心执行池,它继承自 AbstractExecutorServiceForkJoinPool 具有工作窃取算法,可以有效管理多线程的任务执行。

  2. ForkJoinTask ForkJoinTask 是框架中任务的抽象类,包含两个重要的子类:

    • RecursiveTask:表示有返回结果的任务。

    • RecursiveAction:表示没有返回结果的任务。

工作原理

  • 分割任务:通过 fork() 方法将任务分解成多个子任务。

  • 执行任务:通过 join() 方法等待子任务完成并返回结果。

  • 合并结果:当子任务完成后,将它们的结果合并,最终生成父任务的结果。

示例应用场景

  • 计算大规模的 Fibonacci 数列。

  • 并行计算大规模数组的和。

  • 图像处理、搜索引擎等需要进行分治操作的计算任务。

总结

Fork/Join 框架主要用于将一个大任务拆分为多个小任务,并行执行,适合分治型计算。它通过工作窃取算法来优化多线程执行,能够有效利用多核处理器,提高并行计算的性能。

锁与并发优化

乐观锁和悲观锁的区别?

乐观锁悲观锁是两种常见的并发控制机制,用于在多线程环境下处理共享资源的访问,防止数据不一致和竞争条件。它们的主要区别在于对锁的使用和对并发访问的态度不同。

1. 悲观锁(Pessimistic Lock)

  • 概念:悲观锁的思想是认为在多线程环境下,数据会频繁发生冲突,因此在访问数据时,线程会先加锁,其他线程只能等待,直到锁被释放后才能继续执行。换句话说,悲观锁假设在访问共享资源时,必然会发生冲突,因此要采取预防措施(加锁)。

  • 实现方式:通常通过 synchronized 关键字或者 ReentrantLock 等实现。

  • 特点

    • 性能开销较大:因为每次访问资源时都会加锁,导致其他线程必须等待锁释放,可能会引起线程阻塞。

    • 适用于冲突频繁的场景:当多个线程同时操作共享资源的概率较高时,悲观锁可以有效防止数据不一致。

  • 应用场景:适用于并发访问较为激烈的环境,例如银行转账、库存更新等需要保证数据一致性的场景。

2. 乐观锁(Optimistic Lock)

  • 概念:乐观锁的思想是认为在多线程环境下,数据冲突的概率较低,因此在访问数据时,不会加锁,而是直接操作数据。操作完成后再检查数据是否被其他线程修改。如果数据没有被修改,操作就成功;如果数据被修改了,乐观锁会重新尝试操作。这种机制通常依赖于“版本控制”或“比较和交换”机制。

  • 实现方式:常用的是CAS(Compare and Swap),通过比较值是否发生变化来判断是否发生了并发冲突。或者通过数据库中的版本号机制来判断数据是否已被修改。

  • 特点

    • 性能较好:因为乐观锁不会在每次访问时加锁,线程不需要阻塞,减少了锁的开销,性能相对较高。

    • 适用于冲突较少的场景:如果数据访问冲突的概率较低,乐观锁可以减少同步开销,提升并发性能。

  • 应用场景:适用于并发访问较少或冲突不频繁的环境,例如缓存更新、批量数据处理等。

3. 对比总结

特性乐观锁 (Optimistic Lock)悲观锁 (Pessimistic Lock)
锁的使用不加锁,操作前后进行冲突检查每次操作前都加锁
性能性能较高,适用于冲突较少的场景性能较差,适用于冲突频繁的场景
阻塞情况无阻塞,只有在操作完成后检查冲突会阻塞,直到锁被释放才能继续执行
实现方式CAS、版本号控制等synchronizedReentrantLock
适用场景并发访问较少或冲突不频繁的场景并发访问较多,数据一致性要求高的场景

4. 总结

  • 悲观锁适用于数据冲突频繁的场景,通过加锁来防止数据冲突,但可能会带来性能损失。

  • 乐观锁适用于数据冲突较少的场景,通过不加锁的方式提高性能,但需要额外的冲突检测机制(如CAS或版本控制)。

什么是死锁?如何避免死锁?

死锁是什么?

死锁(Deadlock)是指在多个线程之间发生一种特殊的情况,其中每个线程都在等待其他线程释放它所需要的资源,但这些线程都无法继续执行,导致程序进入一个永远等待的状态。简而言之,死锁发生时,程序中的所有线程都在相互等待资源,造成了无休止的等待,从而导致程序无法继续执行。

死锁的必要条件(四个条件)

死锁通常是由以下四个条件共同作用导致的,它们被称为死锁的四个必要条件

  1. 互斥条件(Mutual Exclusion) 至少有一个资源处于“只允许一个线程访问”的状态,也就是说,某个资源只能被一个线程占用。

  2. 请求与保持条件(Hold and Wait) 一个线程持有某些资源,同时又请求其他资源,但是请求的资源被其他线程持有,造成线程阻塞,不能继续执行。

  3. 不剥夺条件(No Preemption) 已分配给线程的资源在未使用完之前不能被强行剥夺,只能在线程完成任务后才释放资源。

  4. 循环等待条件(Circular Wait) 线程集合中的线程相互等待对方持有的资源,形成一个闭环。例如,线程A等待线程B的资源,线程B等待线程C的资源,线程C等待线程A的资源。

当这四个条件同时满足时,就可能发生死锁。

如何避免死锁?

为了避免死锁,可以采取以下几种策略:

1. 避免死锁的策略:资源请求顺序

最常见的一种避免死锁的方法是资源排序。确保所有线程按照一致的顺序申请资源。通过避免循环等待,可以减少死锁的发生。

  • 策略:为所有共享资源分配一个全局顺序,线程必须按照资源的顺序申请资源。这样,可以避免循环等待的条件。

例如,如果有资源A和资源B,线程必须先请求资源A,然后请求资源B,而不能反过来。

2. 使用超时机制

可以为每个线程的资源请求设置一个超时时间。如果线程在超时时间内未能获得资源,它将放弃当前的请求,释放已占用的资源,并尝试重新执行或处理其他任务。

  • 策略:线程在请求资源时使用带有超时限制的 tryLock() 或在数据库中使用 SELECT ... FOR UPDATE 配合 timeout 来避免死锁。

3. 死锁检测

通过使用监控或日志等方式实时检测系统中的死锁情况,发现死锁后采取措施,比如回滚某些操作或强制中断某些线程。

  • 策略:通过定期检查线程间的资源请求情况,检测是否有死锁发生。如果发现死锁,系统可以采取回滚、重试或中断某些线程的操作。

4. 减少持有锁的时间

尽量缩短线程持有锁的时间,确保在获取锁时,尽量减少在锁定资源期间执行的工作,减少死锁的发生概率。

  • 策略:线程在持有锁时,只执行必要的工作,尽早释放锁。避免在持有锁时执行耗时操作或其他可能引发阻塞的任务。

5. 使用更高层次的锁机制

某些高级锁机制,如 ReentrantLock 提供了内置的死锁预防机制(如 tryLock())和超时处理机制,能够更好地控制锁的获取与释放,避免死锁。

6. 锁粒度控制

控制锁的粒度,尽量减少每次获取锁的资源范围,避免多个锁的竞争。可以通过分层锁、细化锁的粒度来降低死锁的概率。

  • 策略:尽量避免锁定多个资源,或者尽量减少锁定资源的数量。

总结

  • 死锁 是指在多线程环境下,多个线程互相等待资源,导致程序无法继续执行的情况。

  • 避免死锁的策略 主要包括资源请求顺序、超时机制、死锁检测、减少锁持有时间、使用高层锁机制和控制锁的粒度等方法。

  • 为了有效防止死锁,通常需要根据具体的应用场景和锁策略做出适当的调整和优化。

AQS(AbstractQueuedSynchronizer)的作用?

AQS(AbstractQueuedSynchronizer)是 Java 并发包(java.util.concurrent)中的一个抽象类,提供了一种基于队列的同步器框架,用于构建自定义的同步工具(如 ReentrantLockCountDownLatchSemaphore 等)。它是实现各种同步机制的基础框架,可以帮助开发者在并发编程中简化锁的实现。

AQS 的作用

AQS 提供了一个统一的抽象框架,用于实现同步器的基本操作,如请求锁、释放锁、排队等待等。它通过一个双向队列(FIFO 队列)管理多个线程,确保线程按照请求的顺序访问共享资源。AQS 主要负责以下几项工作:

  1. 队列管理 AQS 使用一个队列来管理线程的排队。在同步操作过程中,如果某个线程无法立即获取到锁或资源,它会被放入队列中,等待其他线程释放资源后再进行获取。AQS 管理这些线程的入队、出队、等待和唤醒操作。

  2. 线程的获取与释放 AQS 提供了获取和释放同步资源的基本操作,如 acquire()release()。通过继承 AQS 并实现其中的抽象方法,开发者可以根据自己的需求定制不同的同步器。

  3. 共享和独占模式 AQS 支持两种不同的同步模式:独占模式和共享模式。

    • 独占模式:某个线程获取资源后,其他线程无法获取该资源,直到持有资源的线程释放。

    • 共享模式:多个线程可以同时获取资源,直到资源达到上限才会阻塞等待。

  4. 线程阻塞与唤醒 AQS 管理线程的阻塞与唤醒机制。如果当前线程无法获取到资源,它会被加入队列并进入阻塞状态。其他线程释放资源后,会唤醒队列中的线程,使其能够继续执行。

  5. 底层支持自定义同步器 通过继承 AQS 并实现其方法,开发者可以轻松实现自定义的同步器。比如 ReentrantLockCountDownLatchSemaphoreReadWriteLock 等都可以通过 AQS 来实现。

AQS 的核心方法

  • acquire(int arg): 请求获取资源,并尝试根据给定的参数(如尝试次数、超时等)获得同步资源。通常用于实现锁的获取逻辑。

  • release(int arg): 释放资源,表示当前线程完成任务后释放锁或者同步资源。通常用于实现锁的释放逻辑。

  • tryAcquire(int arg): 尝试获取资源,通常会被自定义同步器重写,以决定是否能够立即获取锁。

  • tryRelease(int arg): 尝试释放资源,通常会被自定义同步器重写,执行一些资源释放后的后处理操作。

  • acquireShared(int arg): 共享模式下请求资源。通常用于如信号量等共享资源的获取。

  • releaseShared(int arg): 共享模式下释放资源,通常用于信号量等资源的释放。

AQS 的工作原理

  1. 队列和线程阻塞 当一个线程请求获取资源时,如果该资源当前不可用,线程将被加入到 AQS 的等待队列中。线程进入等待状态,直到有线程释放资源,并唤醒它。

  2. 资源的竞争和获取 资源的获取通常由 tryAcquiretryAcquireShared 方法实现。如果这些方法成功获取了资源,线程就可以开始执行。如果失败,则进入队列,等待被唤醒。

  3. 资源的释放 线程完成任务后,通过 releasereleaseShared 方法释放资源。此时,AQS 会尝试唤醒队列中的其他线程,让它们有机会获取资源。

  4. 队列的管理 AQS 会确保线程按照请求的顺序进行排队等待,FIFO 顺序。如果线程获取资源成功,它就会从队列中移除,继续执行。

AQS 的应用

AQS 本身并不会直接用于同步操作,而是作为一个底层工具,帮助开发者构建自定义的同步工具。基于 AQS,Java 提供了很多常见的同步工具,例如:

  • ReentrantLock:可重入锁,实现了独占锁的功能。

  • CountDownLatch:允许一个或多个线程等待其他线程执行完成后再继续执行。

  • Semaphore:限制某个资源的最大并发线程数,控制访问资源的线程数。

  • ReadWriteLock:读写锁,允许多个线程同时读取,但在写操作时独占资源。

  • CyclicBarrier:让一组线程在某个阶段等待,直到所有线程都达到这个阶段。

总结

AQS 是 Java 并发包中的一个核心类,用于构建自定义同步器。它通过一个队列管理线程的排队,支持独占模式和共享模式,提供线程获取和释放资源的基本操作,帮助开发者简化复杂的同步控制。通过 AQS,开发者可以轻松实现高效的并发控制机制,如锁、信号量、计数器等同步工具。

什么是偏向锁、轻量级锁、重量级锁?

偏向锁、轻量级锁和重量级锁是 Java 虚拟机(JVM)中针对线程竞争的不同优化策略。它们的设计目的是为了减少锁竞争和提高性能。它们在 JDK 的不同版本中不断改进,尤其是随着 JDK 1.6 和之后版本的引入,使得锁的性能得到了显著提升。下面是它们的具体解释和区别。

1. 偏向锁(Biased Locking)

偏向锁是 JVM 为了优化单线程场景下的锁竞争而提出的一种锁优化策略。其目的是减少获取锁的开销,特别是在只有一个线程访问同步块时。

  • 偏向锁的工作原理: 偏向锁的基本思想是当一个线程第一次获得锁时,会将锁的标记记录在对象头中,并且在后续获取锁时,不需要做任何的同步操作,直接获取对象的锁。只有当其他线程竞争该锁时,才会撤销偏向锁,转为轻量级锁或重量级锁。

  • 何时使用: 偏向锁适用于绝大多数情况下只有一个线程访问某个对象的场景,比如缓存操作、日志记录等。它减少了每次获取锁时的性能开销。

  • 撤销条件: 偏向锁会在以下情况下被撤销:

    • 另一个线程尝试获取该锁。

    • 当前线程被中断。

    • 当前线程在获取锁时发生了死锁。

2. 轻量级锁(Lightweight Locking)

轻量级锁是为了解决偏向锁撤销后的锁竞争问题而提出的优化机制。它的目标是提高锁的性能,避免每次加锁都进入重量级锁的状态。

  • 轻量级锁的工作原理: 轻量级锁的基本思路是线程在获取锁时,会先尝试使用一个称为锁标记(Lock Record)的结构来判断是否已经有线程持有该锁。这个过程是无锁的,只有在发生锁竞争时,才会转为重量级锁。轻量级锁的实现依赖于 CAS(Compare-And-Swap) 操作,如果 CAS 成功,则锁定成功;否则,如果锁被其他线程占用,则会撤销轻量级锁并进入阻塞状态(进入重量级锁)。

  • 何时使用: 适用于少量线程竞争的场景。如果只有一个线程访问共享资源,轻量级锁能够提供较好的性能。如果有多个线程竞争,轻量级锁会变成重量级锁,导致性能下降。

  • 特点

    • 轻量级锁不需要进行系统调用,尽量避免了进入阻塞队列。

    • 线程只有在竞争时才会升级为重量级锁,从而减少了不必要的锁竞争。

3. 重量级锁(Heavyweight Locking)

重量级锁是传统的锁机制,它通常是指 synchronized 锁。当锁竞争较激烈时,JVM 会将轻量级锁升级为重量级锁。重量级锁会导致线程阻塞和唤醒,通常会引入系统调用,影响性能。

  • 重量级锁的工作原理: 当多个线程争用同一个锁时,JVM 会使用操作系统的互斥机制(例如互斥量或信号量)来保护共享资源。当一个线程获取不到锁时,它会被挂起并进入阻塞状态,直到其他线程释放锁为止。

  • 何时使用: 在高并发情况下,当多个线程争用同一个锁时,轻量级锁无法解决问题,锁会被升级为重量级锁。重量级锁会严重影响性能,导致线程上下文切换和阻塞。

  • 特点

    • 在多线程竞争激烈时,重量级锁会涉及线程的上下文切换和内核调度,性能开销较大。

    • 进入重量级锁后,线程会被阻塞,直到锁释放。

4. 总结

锁类型描述适用场景性能表现
偏向锁优化单线程场景,当只有一个线程竞争时不会进行加锁操作。适用于只有一个线程操作的场景,线程竞争少。性能最好,几乎没有开销。
轻量级锁当多个线程竞争时,采用 CAS 等机制减少锁的开销,避免进入阻塞状态。线程竞争较少的情况。性能优于重量级锁,但在竞争时会升级为重量级锁。
重量级锁传统的锁机制,线程会进入阻塞状态,通过操作系统的机制实现同步。线程竞争激烈的情况。性能差,涉及阻塞、唤醒、上下文切换等开销。

5. JVM 实现锁优化

JVM 会根据运行时的不同情况动态调整锁的状态:

  • 初始时可能是偏向锁,适应单线程环境。

  • 如果有多个线程竞争,偏向锁会升级为轻量级锁。

  • 当轻量级锁无法满足并发需求时,锁会被升级为重量级锁。

这种锁的升级机制是为了尽可能减少锁的开销,提供最佳的性能。因此,JVM 的锁优化是动态的,并且会根据线程竞争的情况做出相应的调整。

什么是自旋锁?

自旋锁是一种同步机制,它通过不断循环检查某个条件(例如锁的状态)来获取锁,而不是让线程进入阻塞状态。线程在获取锁时,如果发现锁已被其他线程占用,它不会立即放弃,而是会持续检查锁是否释放,直到获得锁为止。

自旋锁的特点:

  1. 无阻塞:自旋锁不会让线程进入阻塞状态,它让线程持续检查锁的状态。这样避免了线程阻塞和唤醒的开销,减少了上下文切换。

  2. 适用于锁持有时间短的场景:当锁的持有时间很短时,自旋锁比阻塞式锁(如 synchronized)更高效,因为它避免了线程的上下文切换。

  3. CPU消耗:在高并发情况下,线程会一直自旋等待锁,导致占用大量 CPU 资源。如果锁的持有时间过长,CPU 会被浪费掉。

  4. 没有公平性保证:自旋锁不能保证先到的线程优先获取锁,可能会导致某些线程一直无法获取到锁。

自旋锁的应用场景:

  1. 锁持有时间短的场景:自旋锁适合用于锁持有时间非常短的场景,例如一个线程执行的操作只需要几次 CPU 时钟周期就能完成。

  2. 高并发的场景:在某些高并发场景下,如果大部分时间只有一个线程能成功获取锁,其他线程可以通过自旋快速等待,避免了不必要的上下文切换。

  3. 避免线程阻塞:自旋锁能避免线程被挂起,特别适用于锁竞争较小的场景。

自旋锁的缺点:

  1. CPU占用高:如果锁的持有时间较长,线程将会长时间自旋,占用大量 CPU 资源,降低系统的性能。

  2. 锁竞争激烈时不适用:当多个线程频繁竞争锁时,自旋锁可能会导致严重的性能问题,因为线程会一直消耗 CPU 资源。

  3. 没有公平性:自旋锁不保证锁的获取顺序,因此可能会出现某些线程长时间无法获得锁的情况。

总的来说,自旋锁在某些场景下能提高性能,但在锁竞争激烈或锁持有时间长的情况下,它的缺点会非常明显,因此使用时需要谨慎。

JMM(Java 内存模型)

什么是 JMM?

JMM(Java Memory Model,Java内存模型)是 Java 程序中线程间通信和共享数据的规范,它定义了 Java 程序中不同线程如何访问共享变量、如何同步变量的值,以及如何确保多线程程序的正确执行。JMM 主要目的是确保 Java 程序的可见性、原子性和有序性,以便在多线程环境下避免出现不可预料的行为。

JMM的核心概念

  1. 内存共享: 在 Java 中,所有线程共享一块内存区域。每个线程有自己的工作内存,线程的工作内存存储了它所使用的变量副本,而共享变量存储在主内存中。线程对共享变量的读写操作必须通过主内存来进行。

  2. 主内存和工作内存

    • 主内存:存储所有线程共享的变量,线程通过主内存进行读写操作。

    • 工作内存:每个线程有自己的工作内存,工作内存是线程对变量的本地副本,线程在工作内存中操作共享变量。

  3. JMM的目标: JMM 的设计目标是确保多线程编程中共享变量的可见性、原子性有序性

    • 可见性:当一个线程修改了共享变量的值,其他线程能及时看到这个修改。

    • 原子性:一个操作要么全部执行成功,要么完全不执行,不会受到其他线程的干扰。

    • 有序性:程序中代码的执行顺序必须符合语义,避免由于指令重排序导致的异常行为。

JMM的关键规则

  1. 主内存与工作内存的交互规则

    • 读取共享变量:当线程要读取共享变量时,必须通过工作内存读取主内存中的变量。

    • 写入共享变量:当线程要修改共享变量时,必须将修改的值写入主内存。

  2. volatile变量

    • 使用 volatile 关键字修饰的变量,能够保证线程对该变量的修改对其他线程是可见的。即当一个线程修改了 volatile 变量的值,其他线程立即能够看到该变化。

    • 但是,volatile 并不能保证复合操作的原子性,像 ++ 这种操作仍然会出现线程安全问题。

  3. Happens-Before原则: JMM定义了happens-before原则,用于确定线程间的操作顺序。它描述了不同线程之间的操作是否能够保证顺序执行。

    • 程序顺序规则:一个线程内的操作按照程序的顺序执行。

    • 锁规则:在进入某个锁(如 synchronized)之前的操作,happens-before 锁释放之后的操作。

    • volatile规则:对 volatile 变量的写操作 happens-before 任何后续对该变量的读操作。

  4. 重排序和指令重排: JMM允许一定程度的指令重排序,以提高性能。但这可能会导致程序执行结果与预期不一致。为了避免不必要的重排序,JMM通过同步机制(如 synchronizedvolatile)来保证线程之间的正确执行顺序。

JMM的内存可见性问题

在多线程环境中,由于每个线程有自己的工作内存,线程对共享变量的修改在某些情况下可能不会及时传递到其他线程。例如:

  • 线程1修改共享变量A的值,但线程2未能及时读取到线程1修改后的最新值。这种情况称为内存可见性问题。

解决这个问题的一些方法包括:

  • 使用 volatile 关键字,确保修改立即对其他线程可见。

  • 使用 synchronized 块,确保对共享变量的访问是同步的,避免内存可见性问题。

JMM的原子性和有序性

  1. 原子性:JMM确保一些基本操作(如读取、写入)是原子的,但像 ++ 这样的复合操作不是原子的,需要通过同步机制(例如 synchronizedAtomic 类)来保证原子性。

  2. 有序性:JMM通过允许一定程度的指令重排来提高性能。但为了避免重排序导致的错误,需要使用同步机制来确保正确的执行顺序。比如 synchronizedvolatile 可以确保有序性。

总结

JMM定义了 Java 程序中多线程如何正确地共享数据,它通过规定内存模型的规则,保证了线程间的可见性、原子性和有序性。理解 JMM 可以帮助我们更好地设计多线程程序,避免常见的并发问题。

happens-before 原则是什么?

Happens-Before原则是Java内存模型(JMM)中定义的线程操作顺序的规则,它保证了多线程环境下线程之间的操作顺序及可见性。它的基本意思是:一个操作必须发生在另一个操作之前,从而确保前一个操作的结果能被后续操作看到。

Happens-Before的核心规则

  1. 程序顺序规则:一个线程内的操作总是按照程序顺序执行的,也就是说,前面的操作 happens-before 后面的操作。

  2. 锁规则:在多个线程之间,如果一个线程释放了锁,那么另一个线程在获取该锁时,释放锁的操作 happens-before 获取锁的操作。这样,第二个线程能够看到第一个线程对共享变量的修改。

  3. volatile规则:对 volatile 变量的写操作 happens-before 任何后续对该变量的读操作。即当一个线程修改了 volatile 变量的值,其他线程立刻可以看到修改后的值。

  4. 线程启动规则:当一个线程调用另一个线程的 start() 方法时,start() 操作 happens-before 被启动线程的任何其他操作,确保启动线程的状态是可见的。

  5. 线程结束规则:当一个线程调用 join() 方法等待另一个线程结束时,join() 操作 happens-before 被调用线程的结束操作。确保前一个线程的执行完成,后续的线程才能继续。

  6. 中断规则:线程的中断操作 happens-before isInterrupted() 检查。

为什么Happens-Before很重要?

Happens-Before原则定义了线程之间的操作顺序,确保了在多线程程序中,线程间的共享数据的一致性和可见性。它帮助开发者理解在不同线程之间如何正确地同步数据,从而避免线程安全问题。

什么是内存可见性问题?如何解决?

内存可见性问题是指在多线程环境下,一个线程对共享变量的修改,其他线程可能无法立即看到该修改的情况。这种问题会导致线程间的同步失效,造成程序的行为不可预测。

内存可见性问题的原因:

  1. 线程本地缓存:现代处理器为提高性能,会对线程的工作内存进行优化,每个线程都有自己的一块工作内存,线程对共享变量的修改可能只会影响到自己本地的缓存,而不会立刻同步到主内存中,导致其他线程无法看到这个修改。

  2. CPU重排序:为了提高执行效率,CPU可能会对指令进行重排序,这可能会改变程序中操作的执行顺序,导致一个线程对共享变量的修改在另一个线程访问之前不可见。

  3. 不适当的同步:如果多个线程对共享数据进行操作时没有适当的同步机制,也会导致线程间的共享数据不一致,无法保证修改的可见性。

如何解决内存可见性问题?

  1. 使用 volatile 关键字volatile 关键字保证了对该变量的修改对所有线程是可见的。即每次读取 volatile 变量时,都会直接从主内存中读取最新的值,而不是从线程的工作内存中读取。这就确保了多个线程间对该变量的修改能够及时被其他线程看到。

  2. 使用 synchronized 关键字synchronized 关键字通过加锁来确保共享数据的可见性和原子性。每个线程在进入一个同步块时,必须首先获取锁,并在执行完同步块后释放锁。这样,线程之间的共享变量修改会通过锁的机制同步到主内存,从而保证数据的可见性。

  3. 使用 java.util.concurrent 包中的工具类

    • Atomic:例如 AtomicIntegerAtomicBoolean 等类提供了原子性操作,保证线程安全,并且它们的操作通常是直接通过硬件提供的原子操作实现的。

    • CountDownLatchCyclicBarrier 等同步工具:这些工具类能够帮助线程之间协调执行顺序,从而保证共享变量在不同线程之间的可见性。

  4. 使用 final 关键字: 对于一些不可变的对象,使用 final 关键字可以保证线程安全。当一个对象被标记为 final 时,JVM 会保证该对象的初始化操作是线程安全的,也就是说,final 字段一旦赋值,其他线程就能够立即看到该字段的值。

  5. 使用内存屏障(Memory Barrier): 内存屏障是硬件或操作系统提供的底层机制,用来防止指令重排序,确保操作的顺序性。虽然这种机制通常由 JVM 和底层硬件处理,但我们可以通过使用高层次的同步机制(如 volatilesynchronized)来间接控制内存屏障的使用。

总结

内存可见性问题会导致多线程程序出现不可预知的错误。为了解决这个问题,我们可以通过使用 volatilesynchronized 等同步机制,确保线程间对共享变量的修改能够及时且正确地传播到其他线程,从而保证程序的一致性和可靠性。

final 关键字在多线程中的作用?

抱歉,下面我不会再提供代码,只会解释概念。

在多线程中,final 关键字的作用主要体现在以下几个方面:

1. 保证对象的安全发布

在多线程环境下,当多个线程共享同一个对象时,确保对象在构造完成后能够正确地被其他线程看到是至关重要的。final 关键字能够保证一个对象在构造完成后,其值对其他线程是可见的。这是因为 final 修饰的字段保证了在对象构造过程中,不会出现字段的重排序或值的延迟写入,使得构造完成后的对象状态对其他线程是一致的。

2. 创建不可变对象

final 可以用于字段,确保字段在对象构造后不可修改。通过 final,可以创建不可变对象(Immutable Object)。不可变对象的状态在构造完成后不能被改变,因此在多线程环境下,不需要担心多个线程同时修改对象的状态,会造成数据不一致的问题。这种特性使得不可变对象天然具有线程安全性。

3. 防止重排序

final 关键字还与JVM的内存模型紧密相关。它能确保变量在构造时的初始化顺序,防止指令重排序(即,JVM或CPU为了优化性能而调整指令执行顺序的行为)。这样,可以保证在构造过程中,final 变量的值在对象构造完成之前不会发生变化,确保其他线程能够正确地看到该变量的值。

总结

final 关键字在多线程中的作用主要是:

  • 保证对象构造完成后,其字段在所有线程中都是可见的;

  • 使得对象不可修改,从而避免多个线程修改对象状态造成的竞争条件;

  • 防止指令重排序,确保变量初始化顺序,避免可见性问题。

这些特点使得 final 成为实现线程安全和正确发布共享变量的重要工具。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2341302.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于无障碍跳过广告-基于节点跳过广告

2025-04-22 一些广告的关闭是叉图标,获取到的信息也没什么特征,这种广告怎么跳过 用autojs无障碍的节点定位ui控件位置,点击

element-ui、element-plus表单resetFields()无效的坑

一、基本前提: 1、form组件上必须要有ref 2、form-item上必须要有prop属性 二、新增/编辑用一个el-dialog时,先新增再编辑没问题,先编辑再新增未清空 原因 在没有点新增或着编辑时,我的el-dialog弹出框里的内容是空白的&…

计算机视觉算法实现——救生衣穿戴状态智能识别

✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连 ✨ ✨个人主页欢迎您的访问 ✨期待您的三连✨ ​​​​ ​​​​​​​​​​​​ ​​​​ 一、救生衣穿戴状态识别领域概述 水上安全一直是全球关注的重大问题,据世界卫生组…

Science Robotics 新型层级化架构实现250个机器人智能组队,“单点故障”系统仍可稳定运行

近期,比利时布鲁塞尔自由大学博士生朱炜煦与所在团队提出了一种创新的机器人群体架构——“自组织神经系统”(SoNS,Self-organizing Nervous System)。 它通过模仿自然界中的生物神经系统的组织原理,为机器人群体建立了…

手写深拷贝函数

在 JavaScript 中,深拷贝是指创建一个对象或数组的完全独立副本,包括其嵌套的对象或数组。这意味着修改副本不会影响原始对象。 以下是手写一个通用的深拷贝函数的实现: 深拷贝函数实现 function deepClone(target, map new WeakMap()) {//…

React 性能优化三剑客实战:告别无效重渲染!

在 Vue 中我们可能依赖 Vuex computed 进行状态共享和性能优化,而在 React 里呢?不需要用 Redux,靠 useContext、memo、useMemo 三剑客就能构建高性能组件通信方案! 🧩 useContext 再回顾:状态共享不等于性…

APP动态交互原型实例|墨刀变量控制+条件判断教程

引言 不同行业的产品经理在绘制原型图时,拥有不同的呈现方式。对于第三方软件技术服务公司的产品经理来说,高保真动态交互原型不仅可以在开发前验证交互逻辑,还能为甲方客户带来更直观、真实的体验。 本文第三部分将分享一个实战案例&#…

色谱图QCPColorMap

一、QCPColorMap 概述 QCPColorMap 是 QCustomPlot 中用于绘制二维颜色图的类,可以将矩阵数据可视化为颜色图(热力图),支持自定义色标和插值方式。 二、主要属性 属性类型描述dataQCPColorMapData存储颜色图数据的对象interpol…

最新扣子(Coze)案例教程:飞书多维表格按条件筛选记录 + 读取分页Coze工作流,无限循环使用方法,手把手教学,完全免费教程

大家好,我是斜杠君。 👨‍💻 星球群里有同学想学习一下飞书多维表格的使用方法,关于如何通过按条件筛选飞书多维表格中的记录,以及如何使用分页解决最多一次只能读取500条的限制问题。 斜杠君今天就带大家一起搭建一…

Spring AI Alibaba-02-多轮对话记忆、持久化消息记录

Spring AI Alibaba-02-多轮对话记忆、持久化消息记录 Lison <dreamlison163.com>, v1.0.0, 2025.04.19 文章目录 Spring AI Alibaba-02-多轮对话记忆、持久化消息记录多轮对话对话持久-Redis 本次主要聚焦于多轮对话功能的实现&#xff0c;后续会逐步增加更多实用内容&…

联邦元学习实现个性化物联网的框架

随着数据安全和隐私保护相关法律法规的出台&#xff0c;需要直接在中央服务器上收集和处理数据的集中式解决方案&#xff0c;对于个性化物联网而言&#xff0c;训练各种特定领域场景的人工智能模型已变得不切实际。基于此&#xff0c;中山大学&#xff0c;南洋理工大学&#xf…

实验1 温度转换与输入输出强化

知识点&#xff1a;input()/print()、分支语句、字符串处理&#xff08;教材2.1-2.2&#xff09; 实验任务&#xff1a; 1. 实现摄氏温度与华氏温度互转&#xff08;保留两位小数&#xff09; 2. 扩展功能&#xff1a;输入错误处理&#xff08;如非数字输入提示重新输入&#x…

【AI】SpringAI 第五弹:接入千帆大模型

1. 添加依赖 <dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-qianfan</artifactId> </dependency> 2. 编写 yml 配置文件 spring:ai:qianfan:api-key: 你的api-keysecret-key: 你的secr…

[Godot] C#2D平台游戏基础移动和进阶跳跃代码

本文章给大家分享一下如何实现基本的移动和进阶的跳跃&#xff08;跳跃缓冲、可变跳跃、土狼时间&#xff09;以及相对应的重力代码&#xff0c;大家可以根据自己的需要自行修改 实现效果 场景搭建 因为Godot不像Unity&#xff0c;一个节点只能绑定一个脚本&#xff0c;所以我…

【Unity笔记】Unity + OpenXR项目无法启动SteamVR的排查与解决全指南

图片为AI生成 一、前言 随着Unity在XR领域全面转向OpenXR标准&#xff0c;越来越多的开发者选择使用OpenXR来构建跨平台的VR应用。但在项目实际部署中发现&#xff1a;打包成的EXE程序无法正常启动SteamVR&#xff0c;或者SteamVR未能识别到该应用。本文将以“Unity OpenXR …

使用 rebase 轻松管理主干分支

前言 最近遇到一个技术团队的 dev 环境分支错乱&#xff0c;因为是多人合作大家各自提交信息&#xff0c;导致出现很多交叉合并记录&#xff0c;让对应 log 看起来非常混乱&#xff0c;难以阅读。 举例说明 假设我们有一个项目&#xff0c;最初develop分支有 3 个提交记录&a…

【愚公系列】《Python网络爬虫从入门到精通》063-项目实战电商数据侦探(主窗体的数据展示)

&#x1f31f;【技术大咖愚公搬代码&#xff1a;全栈专家的成长之路&#xff0c;你关注的宝藏博主在这里&#xff01;】&#x1f31f; &#x1f4e3;开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主&#xff01; &#x1f…

HttpSessionListener 的用法笔记250417

HttpSessionListener 的用法笔记250417 以下是关于 HttpSessionListener 的用法详解&#xff0c;涵盖核心方法、实现步骤、典型应用场景及注意事项&#xff0c;帮助您全面掌握会话&#xff08;Session&#xff09;生命周期的监听与管理&#xff1a; 1. 核心功能 HttpSessionLi…

火山RTC 5 转推CDN 布局合成规则

实时音视频房间&#xff0c;转推CDN&#xff0c;文档&#xff1a; 转推直播--实时音视频-火山引擎 一、转推CDN 0、前提 * 在调用该接口前&#xff0c;你需要在[控制台](https://console.volcengine.com/rtc/workplaceRTC)开启转推直播功能。<br> * 调…

Spark两种运行模式与部署

1. Spark 的运行模式 部署Spark集群就两种方式&#xff0c;单机模式与集群模式 单机模式就是为了方便开发者调试框架的运行环境。但是生产环境中&#xff0c;一般都是集群部署。 现在Spark目前支持的部署模式&#xff1a; &#xff08;1&#xff09;Local模式&#xff1a;在本地…