热门八股-JUC

发布时间:2026/8/17 21:23:48
热门八股-JUC 线程基础1.进程和线程的区别进程可以理解为一个正在运行的程序比如启动一个JVM就是一个进程。线程是进程里面的执行单元一个进程可以有多个线程。常见的区别进程是资源分配最小单位线程是CPU调度最小单位属于进程进程上下文切换开销大线程切换开销小进程之间相互隔离同一个线程共享堆、方法区一个进程崩溃通常不会影响其他进程线程之间可以直接访问共享资源会引发并发安全问题2.Go的协程和Java线程的区别Go goroutine是用户态轻量级协程由Go的GMP模型调度。Java线程是操作系统内核线程由OS操作系统内核调度。每一个Java线程直接映射操作系统的一个线程Java默认栈1M开销大无法大量创建。Go初始栈2KB可以大量创建goroutine适合大量并发IO任务Java也可以用线程池、NIO、虚拟线程来提高并发能力3.线程有几种创建方式各自优缺点1.继承Thread类写法简单直接调用start()启动。Java是单继承任务和线程机制耦合在一起2.实现Runnable接口任务和线程解耦适合资源共享。run没有返回值不能抛出受检异常3.实现CallableFutureTaskcall()有返回值可以抛出异常可以拿到线程执行结果。get()获取结果会阻塞主线程4.线程池ExecutorService生产环境首选线程复用控制并发减少创建销毁开销4.线程的五大生命周期状态及流转过程JVM线程状态Thread.State一共6种枚举面试常说五大生命周期新建、就绪、运行、阻塞、终止。NEW创建了线程对象还没有调用start()READY调用start()后进入就绪状态RUNNING线程正在执行run()里面的代码BLOCKED:阻塞等待获取synchronized锁WAITING调用wait(),join()无限等待TIMED_WAITINGsleep(time),wait(time),join(time)等待指定时间TERMINATED线程执行完毕流转过程创建线程后是NEW调用start()进入RUNNABLE(包括就绪和运行)竞争锁失败进入BLOCKED调用等待类方法进入WAITING或TIMED_WAITING任务执行完进入TERMINATED5.什么是守护线程和用户线程的区别应用场景守护线程为用户线程提供服务的后台线程只有存在用户线程JVM就不会停止当所有用户线程结束无论守护线程是否执行完JVM直接退出守护线程强制终止GC垃圾回收线程是典型的守护线程main主线程是用户线程应用场景适合做后台监控、定时日志、GC、清理缓存6.sleep()、wait()、yield()、join()区别sleep()让线程休眠指定一段时间不会释放锁wait()当前线程等待释放监视器锁必须写在synchronized同步代码块里面yield()提示调度器礼让CPU但不一定采纳依然拥有锁join()等待另一个线程执行完毕底层就是调用wait()会释放锁7.为什么wait、notify要在synchronized里面执行wait、notify依赖对象监视器锁必须放在synchronized里面主要是为了防止信号丢失问题。synchronized保证条件判断和wait操作是受锁保护的wait执行的时候会释放监视器锁被notify唤醒之后还需要重新竞争锁。如果不在同步代码块中调用会抛出IllegalMonitorStateExceptionwait一定要写在while循环里面判断条件不要用if否则会虚假唤醒。8.什么是线程上下文切换为什么会耗时间上下文切换是CPU从一个线程切到另一个线程执行。切换时要保存当前线程的寄存器、程序计数器、栈等信息再恢复另一个线程的信息。耗时的原因需要保存、恢复CPU寄存器内存读写慢于寄存器线程切换会造成CPU高速缓存失效缓存命中率降低频繁阻塞。唤醒、抢锁会增加调度开销锁机制和关键字1.synchronized底层原理synchronized是JVM层面的内置锁它依赖对象头里的Mark Word和Monitor实现当线程进入同步代码块时会尝试获取对象的monitor获取成功就执行失败就阻塞等待。退出同步代码块时释放monitor。修饰普通方法时锁对象是当前实例this修饰静态方法时锁对象是当前类的Class对象修饰代码块时锁对象是括号里指定法对象。2.synchronized锁升级流程偏向锁-轻量级锁-重量级锁偏向锁只有一个线程反复进入锁偏向这个线程减少CAS轻量级锁多个线程交替竞争用CAS尝试获取锁避免直接阻塞重量级锁竞争激烈时膨胀为Monitor线程阻塞和唤醒由操作系统参与。锁只会升级3.偏向锁、轻量级锁、重量级锁各自原理和使用场景偏向锁底层CAS记录线程ID适合单线程反复获取锁几乎无竞争轻量级锁底层CAS自适应自旋适合线程交替执行竞争不激烈重量级锁底层操作系统Monitor互斥锁适合竞争激烈线程阻塞挂起4.synchronized修饰普通方法、静态方法、代码块锁的对象分别是普通方法当前实例对象this静态方法当前类的Class对象同步代码块synchronized(lock)里的lock对象两个实例对象调用同一个普通同步方法不互斥因为它们的锁是不同的this5.vloatile关键字的作用是什么vloatile主要有两个作用保证可见性一个线程修改变量其他线程可以及时看见禁止指令重排序通过内存屏障保证特点顺序6.为什么vloatile不能保证原子性vloatile只能保证单次读写的可见性不能保证复合操作原子性比如count分为读取、计算、写回。多个线程同时执行时仍可能丢失更新。7.什么是可重入锁为什么需要可重入可重入锁同一个线程已经持有锁还可以再次获取同一把锁不会把自己阻塞。底层依靠计数器重入次数每获取一次计数器1释放一次-1计数器归0才真正释放如果锁不可重入自己已经持有锁再次申请锁线程把自己阻塞直接死锁。synchronized、ReentrantLock都是可重入锁8.什么是公平锁、非公平锁优缺点公平锁严格按照线程请求锁的先后顺序分配锁非公平锁允许新来的线程可以直接插队竞争锁公平锁优点是不会出现线程饥饿缺点性能较差吞吐量低非公平锁优点性能高吞吐量好但可能出现线程饥饿synchronized只能是非公平锁ReentrantLock默认是非公平锁9.悲观锁和乐观锁的区别使用场景悲观锁认为冲突经常发生所以先加锁再操作。缺点有阻塞、死锁上下文切换开销。synchronized、ReentrantLock都属于悲观思路乐观锁认为冲突不一定发生先不加锁提交更新时再检查有没有被其他人改过。CAS、版本号机制都属于乐观思路。使用场景写多冲突多悲观锁更稳读多冲突少乐观锁性能更好10.CAS原理是什么自旋、Unsafe类作用CAS全称CompareAndSwap比较并交换。它有三个值内存值V、期望值A、新值B。只有当VA时才把V改成B否则失败CAS一次可能失败通过while循环不断重试CAS线程不阻塞在Java中很多原子类底层通过Unsafe或VarHandle调用CPU原子指令完成CAS11.CAS三大问题ABA、循环耗时、只能保证单个变量原子性怎么解决ABA变量从A变成B又改回ACAS看不出它变过。解决方法加加版本号同时校验数据和版本号。循环耗时竞争激烈时一直自旋会浪费CPU。解决方法是控制重试次数、退避策略或在高竞争场景改用锁。只能保证单个变量原子性多个变量需要一起更新时可以用锁或者把多个字段封装成一个对象引用通过AtomicReference更新整体。12.什么是自旋锁优缺点自旋锁线程获取锁失败时不阻塞原地循环不断尝试抢锁。优点避免线程阻塞和上下文切换锁持有时间很短的时候性能很好缺点锁占用时间长会大量空循环消耗CPU适用于锁持有时间短、并发竞争不激烈。13.什么是死锁死锁产生的条件是什么死锁多个线程互相持有对方需要的锁互相等待释放全部阻塞程序卡死死锁四个必要条件必须同时满足互斥资源互斥同一个时间只能一个线程占用请求与保持线程持有部分锁不释放继续申请其他的锁不可剥夺锁不能被其他线程强行抢走循环等待线程之间形成环形等待链条破坏任意一个条件就可以避免死锁比如固定加锁顺序加超时时间尽量减少锁嵌套14.什么是偏向锁撤销、重偏向偏向锁撤销是指当另一个线程来竞争被偏向的锁时JVM需要撤销原来的偏向状态。重偏向是指如果一批对象总是从线程A转移到线程B使用JVM可能把这批对象重新偏向线程B减少后续撤销成本。这是JVM锁优化里的细节面试一般讲到“偏向锁适合无竞争竞争出现会撤销并升级”15.轻量级锁自旋次数自适应自适应自旋是重量级锁的优化。JVM会根据历史抢锁记录动态调整自旋循环次数之前自旋成功就多旋几次经常失败就少旋甚至不旋避免空耗CPU16.什么是锁粗化、锁消除JVM优化手段锁粗化如果连续多次对同一个锁反复加锁释放JVM把多段小锁合成一把大锁减少频繁加解锁带来的性能消耗锁消除如果JVM通过逃逸分析发现对象不会被多个线程共享就可以直接去掉锁。AQS机制1.AQS是什么核心设计思想AQS全称AbstractQueuedSyncheonizer抽象队列同步器是JUC里很多同步器的基础框架。核心思想用一个volatile int state表示同步状态再用一个FIFO双向队列管理抢锁失败的线程如果获取资源成功线程继续执行如果失败就封装成节点加入等待队列等待前驱节点释放资源后再被唤醒。2.AQS底层结构state状态、双向阻塞队列state表示同步状态比如锁是否被占用、重入次数、剩余许可数CLH变体双向队列保存等待获取资源的线程节点ReentrantLockstate0,没有线程持有锁。0线程持有锁、可重入获取失败的线程会进入队列并挂起释放锁时唤醒后继节点3.AQS独占模式、共享模式的区别独占模式同一时刻只能有一个线程获取资源。典型例子是ReentrantLock.共享模式同一时刻可以有多个线程获取资源。典型例子是Semaphore CountDownLatch ReadLock区别在于独占模式释放资源通常只唤醒一个后继节点共享模式释放资源可能继续向后传播唤醒多个等待线程。4.AQS排队、唤醒线程的流程获取资源时先尝试获取资源获取失败后当前线程封装成Node加入AQS队列尾部如果前驱节点是head再次尝试获取锁仍失败则挂起线程释放资源时修复state如果资源完全释放唤醒后继节点后继节点被唤醒后继续尝试获取资源5.AQS为什么要用双向链表双向链表方便节点入队、取消、唤醒后继节点线程等待过程中可能被中断或超时取消如果是单向链表删除或跳过取消节点会比较麻烦。双向链表可以通过前驱和后继更方便维护队列JUC常用工具类1.ConcurrentHashMap工作流程ConcurrentHashMap是线程安全的HashMap。JDK8中主要通过CASsynchronized分桶结构保证并发安全写入流程根据key计算hash定位桶位置如果桶为空用CAS放入新节点如果桶不为空对桶头节点加synchronized再插入链表或红黑树如果元素达到阈值则触发扩容读操作通常不加锁通过volatile保证可见性所以读性能比较好2.ReentrantLock和synchronized的区别synchronized是JVM层面锁代码执行完毕或发生异常会自动释放锁ReentrantLock是API锁底层基于AQS实现需要手动lock并且一定要在finally里面unlocksynchronized只能是非公平锁ReentrantLock既可以是公平锁也可以是非公平锁默认非公平。ReentrantLock支持可中断获取锁ReentrantLock支持超时获取锁ReentrantLock支持多个队列Conditionsynchronized代码更简洁不容易忘记释放锁3.ReentrantLock可重入、可中断、可超时、公平锁怎么实现可重入内部用state记录次数同一个线程再次获取锁时state1释放时-1可中断lockInterruptible()在等待锁时可以响应中断可超时tryLock(timeout,unit)在指定时间内获取不到锁就返回失败公平锁获取锁前先判断AQS队列里有没有排队更久的线程有就排队4.ConcurrentHashMapJDK7和JDK8底层原理是区别JDK7使用分段锁Segment,把整个Map分成16个小HashMap,锁粒度是SegmentJDK8取消Segment改为数组链表红黑树锁粒度为桶级别。空桶用CAS插入非空桶用synchronized锁桶头节点5.ConcurrentHashMap为什么JDK8放弃分段锁改用CASsynchronized1.锁粒度更小并发性能更好2.synchronized经过锁升级优化再配合CAS可以让空桶插入无锁化冲突时只锁一个桶3.减少了内存开销6.ConcurrentHashMap的扩容机制JDK8 ConcurrentHashMap支持多线程协助扩容当元素达到阈值线程创建新数组。迁移时每个线程负责一段桶迁移完成的桶会放置ForwardingNode如果其他线程访问到它会帮忙扩容。这叫协助扩容可以避免单个线程扩容时间过长。7.BlockingQueue阻塞队列的核心作用BlockingQueue是支持阻塞插入和阻塞获取的队列当队列满时生产者放入元素会阻塞当队列为空时消费者获取元素会阻塞。安全实现生产者消费者模型是线程池任务队列的核心组件。8.常见阻塞队列的区别和场景1.ArrayBlockingQueue基于数组实现必须指定固定容量有界队列适合固定容量资源可控的场景2.LinkedBlockingQueue单向链表结构默认容量很大也可以指定容量。适合高并发生产消费需要提升吞吐3.SynchronousQueue不存储任何元素生产者必须等消费者直接接受适合快速移交任务4.PriorityBlockingQueue无界数组实现的优先级队列元素必须实现Comparable接口或传入比较器按优先级排序取出。适合任务有优先级区分5.DelayQueue延迟队列只有元素到期后才能被取出适合定时任务、限时缓存9.DelayQueue原理延时任务应用场景DelayQueue基于优先级堆实现队首是最早到期的元素。元素必须实现Delayed接口。任务到期才能被取出未到期则消费线程阻塞多用于订单超时、优惠卷过期、延时重试、定时提醒10.CompletableFuture的常用APICompletableFuture常用于异步编排常见APIsupplyAsync()异步执行有返回值runAsync()异步执行无返回值thenApply()处理上一步结果并返回新结果thenAccept()消费结果无返回值thenCompose()串行依赖避免嵌套FuturethenCombine()合并两个异步任务结果allOf()等待多个任务全部完成anyOf()任意一个任务完成即可exceptionally()异常兜底hendle()正常和异常都能处理它比Future更强因为支持链式调用任务编排和异常处理线程池1.线程池七大核心参数含义ThreadPoolExecutor七大参数:corePoolSize核心线程数maximumPoolSize最大线程数keepAliveTime非核心线程空闲多久被回收unit时间单位workQueue任务队列threadFactory线程工厂用于创建线程handler拒绝策略2.线程池工作原理执行流程当调用execute()提交任务时1.运行线程数小于核心线程数直接创建核心线程执行任务2.核心线程满了任务进入阻塞队列3.队列满了且线程数小于最大线程数创建非核心线程执行4.线程数达到最大值且队列也满了执行拒绝策略3.线程池有哪些种类FixedThreadPool固定线程数量空闲线程不会回收CacheThreadPool线程数可动态扩展SingleThreadExecutor单线程池ScheduledThreadPool支持定时和周期任务WorkStealingPool空闲线程窃取其他队列任务不建议使用Executors创建线程池推荐手动指定参数4.四种拒绝策略分别是什么适用场景AbortPolicy:默认策略直接抛异常。适合必须感知任务失败的场景CallerRunsPolicy让提交任务的线程自己执行。适合削峰让提交速度慢下来DiscardPolicy直接丢弃任务不抛异常。适合允许丢弃的低价值任务DiscardOldestPolicy:丢弃队列中最老的任务再尝试提交新任务。适合更关注新任务的场景5.JDK内置四大线程池特点、坑点FixedThreadPool固定线程数量默认使用无界队列任务堆积可能OOMCacheThreadPool使用SynchronousQueue最大线程数接近无限。高并发下可能创建大量线程导致OOMSingleThreadExecutor单线程串行执行但也是无界队列任务堆积有风险ScheduledThreadPool支持定时和周期任务但也要注意异常处理和任务堆积所以阿里不推荐直接用Executors而是手动创建ThreadPoolExecutor.6.为什么阿里禁止使用Executors创建线程池因为Executors的一些默认参数隐藏风险FixedThreadPool和SingleThreadExecutor使用无界队列可能导致内存溢出。CacheThreadPool最大线程数过大可能创建过多线程。默认线程名不清晰不利于排查问题手动创建线程池可以明确核心线程数、最大线程数、队列大小、线程名和拒绝策略更加可控7.核心线程数怎么合理设置IO密集型、CPU密集型公式CPU密集型任务主要消耗CPU比如计算、加密、压缩。线程数一般设置为 CPU核心数1IO密集型任务会大量等待IO比如数据库、RPC、文件读写、线程数可以更大常见估算CPU核心数*2 通用公式CPU核心数*1等待时间/计算时间8.线程池空闲线程回收机制非核心线程空闲时间超过keepAliveTime就会被回收默认情况下核心线程不会因为空闲被回收可以调用allowCoreThreadTimeOut(true)超时后也会回收。9.线程池关闭shutdown()和shutdownNow()区别shutdown()是温和关闭不在接受新任务但会继续执行队列已有任务.shutdownNow()是立即关闭尝试中断正在执行的任务并返回队列中还没有执行的任务。shutdownNow()只是发出中断信号任务是否停止取决于任务代码是否正确响应中断。10.线程池任务提交execute()和submit()区别execute()只能提交Runnable没有返回值任务异常会直接抛到线程的异常处理器。submit()可以提交Runnable或Callable返回Future可以通过Future.get()获取结果或异常submit()里的异常会被包装进Future如果不调用get()异常可能被忽略。11.线程池异常怎么捕获任务内部try-catch使用submit()后调用Future.get()。自定义线程工厂设置UncaughtExceptionHandle。继承ThreadPoolExecutor重写afterExecute()。生产中建议任务内部做好异常兜底并配合日志、监控和告警。ThreadLocal1.ThreadLocal原理底层ThreadLocalMap结构ThreadLocal用来保存线程本地变量每个线程都有自己独立的一份数据它是底层不是ThreadLocal自己存数据而是每个Thread对象内部有一个ThreadLocalMap。Thread-ThreadLocalMap-Entry(ThreadLocal,value)ThreadLocal对象作为key真正是值是value。不同线程访问同一个ThreadLocal拿到的是各自线程里的value。2.ThreadLocal为什么会内存泄露ThreadLocalMap的key是弱引用value是强引用。当ThreadLocal对象没有外部强引用时key可能被GC回收变成null。但value还被当前线程的ThreadLocalMap强引用着。如果线程一直不结束比如线程池里的工作线程value就可能长期无法被释放造成内存泄露。所以用完一定要调用remove()3.弱引用在ThreadLocal里的作用当外部不在引用ThreadLocal时key可以被GC回收避免key永远留在Map里但弱引用只能解决key的回收value仍然需要通过remove()清理弱引用降低了key泄露风险但不代表不会发生内存泄露4.ThreadLocal有什么缺点可能内存泄露需要手动remove()在线程池中容易出现数据串用因为线程会复用默认不能跨线程传递值过度使用会让数据流向不清晰代码可读性变差总结volatile保证可见性和有序性不保证复合操作原子性synchronized是JVM内置锁锁对象可以是thisClass对象或指定对象CAS是乐观锁思想靠比较并交换实现原子更新AQS用state表示同步状态用FIFO队列管理等待线程ReentrantLock比syncheonized功能更丰富支持可中断、超时、公平锁和多个条件队列ConcurrentHashMapJDK8是数组链表红黑树配合CAS和syncheonized线程池执行顺序的核心线程、队列、最大线程、拒绝策略生产环境不建议用Executors因为默认队列或最大线程数可能不可控ThreadLocal的值在线程内部保存线程池场景用完必须remove()