当前位置: 首页 > news >正文

热门八股-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.实现Callable+FutureTask

call()有返回值可以抛出异常,可以拿到线程执行结果。get()获取结果会阻塞主线程

4.线程池ExecutorService

生产环境首选,线程复用,控制并发,减少创建销毁开销

4.线程的五大生命周期状态及流转过程?

JVM线程状态Thread.State一共6种枚举,面试常说五大生命周期:新建、就绪、运行、阻塞、终止。

  • NEW:创建了线程对象,还没有调用start()
  • READY:调用start()后,进入就绪状态
  • RUNNING:线程正在执行run()里面的代码
  • BLOCKED:阻塞等待获取synchronized锁
  • WAITING:调用wait(),join(),无限等待
  • TIMED_WAITING:sleep(time),wait(time),join(time),等待指定时间
  • TERMINATED:线程执行完毕

流转过程:创建线程后是NEW,调用start()进入RUNNABLE(包括就绪和运行),竞争锁失败进入BLOCKED,调用等待类方法进入WAITINGTIMED_WAITING,任务执行完进入TERMINATED

5.什么是守护线程?和用户线程的区别?应用场景

守护线程:为用户线程提供服务的后台线程

只有存在用户线程,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唤醒之后,还需要重新竞争锁。如果不在同步代码块中调用,会抛出IllegalMonitorStateException

wait一定要写在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对象

两个实例对象调用同一个普通同步方法,不互斥,因为它们的锁是不同的this

5.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。只有当V=A时,才把V改成B,否则失败

CAS一次可能失败,通过while循环不断重试CAS,线程不阻塞

在Java中,很多原子类底层通过Unsafe或VarHandle调用CPU原子指令完成CAS

11.CAS三大问题:ABA、循环耗时、只能保证单个变量原子性,怎么解决?

ABA:变量从A变成B,又改回A,CAS看不出它变过。解决方法加加版本号,同时校验数据和版本号。

循环耗时:竞争激烈时一直自旋会浪费CPU。解决方法是控制重试次数、退避策略,或在高竞争场景改用锁。

只能保证单个变量原子性:多个变量需要一起更新时,可以用锁,或者把多个字段封装成一个对象引用,通过AtomicReference更新整体。

12.什么是自旋锁,优缺点?

自旋锁:线程获取锁失败时,不阻塞,原地循环不断尝试抢锁。

优点:避免线程阻塞和上下文切换;锁持有时间很短的时候性能很好

缺点:锁占用时间长,会大量空循环消耗CPU

适用于锁持有时间短、并发竞争不激烈。

13.什么是死锁,死锁产生的条件是什么?

死锁:多个线程互相持有对方需要的锁,互相等待释放,全部阻塞,程序卡死

死锁四个必要条件,必须同时满足:

  • 互斥:资源互斥,同一个时间只能一个线程占用
  • 请求与保持:线程持有部分锁,不释放,继续申请其他的锁
  • 不可剥夺:锁不能被其他线程强行抢走
  • 循环等待:线程之间形成环形等待链条

破坏任意一个条件就可以避免死锁,比如固定加锁顺序,加超时时间,尽量减少锁嵌套

14.什么是偏向锁撤销、重偏向?

偏向锁撤销是指当另一个线程来竞争被偏向的锁时,JVM需要撤销原来的偏向状态。

重偏向是指如果一批对象总是从线程A转移到线程B使用,JVM可能把这批对象重新偏向线程B,减少后续撤销成本。

这是JVM锁优化里的细节,面试一般讲到“偏向锁适合无竞争,竞争出现会撤销并升级”

15.轻量级锁自旋次数自适应

自适应自旋是重量级锁的优化。JVM会根据历史抢锁记录,动态调整自旋循环次数:之前自旋成功就多旋几次,经常失败就少旋甚至不旋,避免空耗CPU

16.什么是锁粗化、锁消除?JVM优化手段?

锁粗化:如果连续多次对同一个锁反复加锁释放,JVM把多段小锁合成一把大锁,减少频繁加解锁带来的性能消耗

锁消除:如果JVM通过逃逸分析发现对象不会被多个线程共享,就可以直接去掉锁。

AQS机制

1.AQS是什么?核心设计思想?

AQS全称AbstractQueuedSyncheonizer,抽象队列同步器,是JUC里很多同步器的基础框架。

核心思想:用一个volatile int state表示同步状态,再用一个FIFO双向队列管理抢锁失败的线程

如果获取资源成功,线程继续执行;如果失败,就封装成节点加入等待队列,等待前驱节点释放资源后再被唤醒。

2.AQS底层结构:state状态、双向阻塞队列

  • state:表示同步状态,比如锁是否被占用、重入次数、剩余许可数
  • CLH变体双向队列:保存等待获取资源的线程节点

ReentrantLock:state=0,没有线程持有锁。>0线程持有锁、可重入

获取失败的线程会进入队列并挂起,释放锁时唤醒后继节点

3.AQS独占模式、共享模式的区别?

  • 独占模式:同一时刻只能有一个线程获取资源。典型例子是ReentrantLock.
  • 共享模式:同一时刻可以有多个线程获取资源。典型例子是Semaphore CountDownLatch ReadLock

区别在于:独占模式释放资源通常只唤醒一个后继节点;共享模式释放资源可能继续向后传播,唤醒多个等待线程。

4.AQS排队、唤醒线程的流程?

获取资源时:

  1. 先尝试获取资源
  2. 获取失败后,当前线程封装成Node加入AQS队列尾部
  3. 如果前驱节点是head,再次尝试获取锁
  4. 仍失败则挂起线程

释放资源时:

  1. 修复state
  2. 如果资源完全释放,唤醒后继节点
  3. 后继节点被唤醒后继续尝试获取资源

5.AQS为什么要用双向链表

双向链表方便节点入队、取消、唤醒后继节点

线程等待过程中可能被中断或超时取消,如果是单向链表,删除或跳过取消节点会比较麻烦。双向链表可以通过前驱和后继更方便维护队列

JUC常用工具类

1.ConcurrentHashMap工作流程

ConcurrentHashMap是线程安全的HashMap。JDK8中主要通过CAS+synchronized+分桶结构保证并发安全

写入流程:

  1. 根据key计算hash,定位桶位置
  2. 如果桶为空,用CAS放入新节点
  3. 如果桶不为空,对桶头节点加synchronized,再插入链表或红黑树
  4. 如果元素达到阈值,则触发扩容

读操作通常不加锁,通过volatile保证可见性,所以读性能比较好

2.ReentrantLock和synchronized的区别

synchronized是JVM层面锁,代码执行完毕或发生异常会自动释放锁

ReentrantLock是API锁,底层基于AQS实现需要手动lock,并且一定要在finally里面unlock

  • synchronized只能是非公平锁,ReentrantLock既可以是公平锁也可以是非公平锁,默认非公平。
  • ReentrantLock支持可中断获取锁
  • ReentrantLock支持超时获取锁
  • ReentrantLock支持多个队列Condition
  • synchronized代码更简洁,不容易忘记释放锁

3.ReentrantLock可重入、可中断、可超时、公平锁怎么实现

可重入:内部用state记录次数,同一个线程再次获取锁时state+1,释放时-1

可中断:lockInterruptible()在等待锁时可以响应中断

可超时:tryLock(timeout,unit)在指定时间内获取不到锁就返回失败

公平锁:获取锁前先判断AQS队列里有没有排队更久的线程,有就排队

4.ConcurrentHashMapJDK7和JDK8底层原理是区别?

JDK7使用分段锁Segment,把整个Map分成16个小HashMap,锁粒度是Segment

JDK8取消Segment,改为数组+链表+红黑树,锁粒度为桶级别。空桶用CAS插入,非空桶用synchronized锁桶头节点

5.ConcurrentHashMap为什么JDK8放弃分段锁,改用CAS+synchronized

1.锁粒度更小,并发性能更好

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的常用API?

CompletableFuture常用于异步编排

常见API:

  • supplyAsync():异步执行,有返回值
  • runAsync():异步执行,无返回值
  • thenApply():处理上一步结果,并返回新结果
  • thenAccept():消费结果,无返回值
  • thenCompose():串行依赖,避免嵌套Future
  • thenCombine():合并两个异步任务结果
  • 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:固定线程数量,默认使用无界队列,任务堆积可能OOM
  • CacheThreadPool:使用SynchronousQueue,最大线程数接近无限。高并发下可能创建大量线程导致OOM
  • SingleThreadExecutor:单线程串行执行,但也是无界队列,任务堆积有风险
  • ScheduledThreadPool:支持定时和周期任务,但也要注意异常处理和任务堆积

所以阿里不推荐直接用Executors,而是手动创建ThreadPoolExecutor.

6.为什么阿里禁止使用Executors创建线程池

因为Executors的一些默认参数隐藏风险:

  • FixedThreadPool和SingleThreadExecutor使用无界队列,可能导致内存溢出。

  • CacheThreadPool最大线程数过大,可能创建过多线程。

  • 默认线程名不清晰,不利于排查问题

手动创建线程池可以明确核心线程数、最大线程数、队列大小、线程名和拒绝策略,更加可控

7.核心线程数怎么合理设置?IO密集型、CPU密集型公式

CPU密集型任务主要消耗CPU,比如计算、加密、压缩。线程数一般设置为 CPU核心数+1

IO密集型任务会大量等待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()。

生产中建议任务内部做好异常兜底,并配合日志、监控和告警。

ThreadLocal

1.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内置锁,锁对象可以是this,Class对象或指定对象
  • CAS是乐观锁思想,靠比较并交换实现原子更新
  • AQS用state表示同步状态,用FIFO队列管理等待线程
  • ReentrantLocksyncheonized功能更丰富,支持可中断、超时、公平锁和多个条件队列
  • ConcurrentHashMapJDK8+是数组+链表+红黑树,配合CAS和syncheonized
  • 线程池执行顺序的核心线程、队列、最大线程、拒绝策略
  • 生产环境不建议用Executors,因为默认队列或最大线程数可能不可控
  • ThreadLocal的值在线程内部保存,线程池场景用完必须remove()
http://www.cnnetsun.cn/news/4070494.html

相关文章:

  • 特斯拉Model 3如何以鲶鱼效应重塑中国新能源汽车产业格局
  • 江西五十铃新款D-MAX谍照解析:中期改款设计、动力与市场前瞻
  • 网盘直链下载怎么玩才不折腾?我的两年亲测与避坑记录
  • 微信读书网页版字体自定义:CSS注入与Tampermonkey脚本实战
  • Linux Shell特殊符号完全指南:从重定向到管道,掌握命令行核心语法
  • 请求绑定与校验
  • 被苹果放弃的老电脑,我用一个免费工具让它重获新生
  • sva日常学习0
  • RuoYi-Vue Pro 完整上手指南:1 小时搭建带权限与审批的企业级后台
  • AI水印技术解析:从SynthID到C2PA标准,开发者如何管理水印可见性
  • 5分钟跑起跨平台QSP播放器:JavaQuestPlayer完整上手与实测体验
  • PCSX2免费开源模拟器完整指南:如何在家用电脑上高清重玩PS2经典游戏
  • 166、Zephyr RTOS调试与测试基础:调试工具链
  • 虚拟机中OpenFOAM-v2012与Paraview完整安装配置指南
  • Cursor免费使用受阻?一份解决设备限制与机器ID重置的完整指南
  • IoT OTA差分升级:bspatch减小固件体积
  • 如何让《暗黑破坏神2》在现代电脑上满帧运行?D2DX宽屏补丁完整上手指南
  • 每天省下一小时重复劳动的炉石传说插件:HsMod到底能帮你做什么
  • 从一脸懵到轻松绕过:Awesome-WAF 帮你 3 步吃透 Web 应用防火墙攻防测试
  • Ryujinx模拟器终极调优攻略:从装不上到丝滑畅玩的完整实操
  • 系统架构设计师考试模拟题库(下):案例分析与论文指导
  • League Akari 英雄联盟客户端工具箱终极指南:自动选人、流程自动化与对局洞察一网打尽
  • Qwen3.8-27B多模态大模型实战:从技术验货到工程化部署全指南
  • Outfit字体实操指南:免费可商用的几何无衬线字体如何一站式打通品牌视觉统一?
  • 汽车测试谍照背后的产品生命周期管理逻辑与市场策略
  • 构建游戏指令智能系统:从数据建模到安全执行的工程实践
  • YOLO 涨点改进|全网独家复现单通道红外小目标增强 光伏板鸟粪微弱热斑识别、分布式光伏无人机巡检全场景有效涨点
  • 5分钟上手DeepL翻译插件:用开源Chrome划词翻译扩展告别来回切换
  • AI算力市场格局:GPU与FPGA的二元竞争与英特尔AI处理器的破局挑战
  • 溶剂桥接电解液:>4C快充硅负极与−55°C极端低温瓶颈如何打破?