Java多线程与JUC相关面试题

本文介绍了并发、并行和串行的区别,强调了并发编程的三要素:原子性、可见性和有序性。讨论了线程的创建方式、生命周期和状态,以及start()和run()方法的使用。详细解释了wait()、sleep()、sleep()与yield()的区别,以及notify()和notifyAll()的差异。此外,还涵盖了Java中断机制、线程池的使用和原理,以及死锁的概念、条件和预防方法。文章还探讨了Runnable和Callable接口、Future、FutureTask、ThreadLocal的作用,以及乐观锁和悲观锁的实现。最后,列举了线程池的优点和常见的并发工具类,如Semaphore、CountDownLatch和CyclicBarrier,并概述了Atomic类的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并发、并⾏、串⾏之间的区别 ?

串⾏:⼀个任务执⾏完,才能执⾏下⼀个任务
并⾏:两个任务同时执⾏
并发:两个任务整体看上去是同时执⾏,在底层,两个任务被拆成了很多份,然后
⼀个⼀个执⾏,站在更⾼的⻆度看来两个任务是同时在执⾏的

并发编程三要素 ?

1、原子性
原子性指的是一个或者多个操作,要么全部执行并且在执行的过程中不被其他操
作打断,要么就全部都不执行。
2、可见性
可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他
线程可以立即看到修改的结果。
3、有序性
有序性,即程序的执行顺序按照代码的先后顺序来执行。

创建线程的几种方式 ?

  • 继承Thread类
  • 实现Runnable接口
  • 实现Callable接口
  • 通过线程池创建

说说线程的生命周期及五种基本状态 ?

新生(Born): 当线程创建后就进入了新生状态

就绪(Runnable): 当调用线程对象的start()方法,线程即为进入就绪状态

运行(Running): 当CPU调度了处于就绪状态的线程时,此线程才是真正的执行

阻塞(Blocking): 处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行

消亡(Running): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期

线程启动时run方法还是start方法 ?

start()
run()是程序的核心逻辑 是抢到时间片要执行的操作

wait()和sleep的区别 ?

wait()是Object类的方法 它会导致当前线程阻塞,但是这种阻塞要先释放锁标记然后计入等待塞
sleep()是Thread类的静态方法,让线程在指定的毫秒内 区普通阻塞状态。

线程的 sleep()方法和 yield()方法有什么区别 ?

(1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机
会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状
态;
(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
(4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用
yield()方法来控制并发线程的执行。

Notify()和 NotifyAll()有什么区别 ?

如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
NotifyAll()会唤醒所有线程,notify只会唤醒一个线程。
NotifyAll()调用后,会将全部线程由等待池移到锁池,然后参与锁的公平竞争,竞争整个则会继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify()只会唤醒一个线程,具体唤醒那个线程由虚拟机控制。

Java 中 interrupted 和 isInterrupted 方法的区别 ?

interrupt:用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。
注意:线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处
理。支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的
中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中
断了,第一次调用 interrupted 则返回 true,第二次和后面的就返回 false 了。
isInterrupted:查看当前中断信号是true还是false

Java 线程数过多会造成什么异常 ?

  • 线程的生命周期开销非常高
  • 消耗过多的 CPU
    资源如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占
    用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争 CPU资源时还将产生其他性能的开
    销。
  • 降低稳定性JVM
    在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素
    制约,包括 JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制
    等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。

同步方法和同步代码块,那个是更好的选择 ?

同步块是更好的选择,因为它不会锁住占整个对象。同步方法会锁住整个对象,这通常会导致停止执行并需要等待获得这个对象对的锁。
同步块更要符合开发调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说可以避免死锁。
请知道一条原则:同步的范围越小越好。

什么是死锁?产生死锁的条件是什么?怎么防止死锁?

当线程A持有独占锁A,并尝试区获取独占锁B的同时,线程B持有独占锁b,并尝试获取独占锁a的情况下,就会发生AB两个线程由于相互持有对方需要的锁,从而发生阻塞的现象,我们称为死锁。
产生死锁的必要条件:
1、互斥条件:所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。
防止死锁可以采用以下的方法:
尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、
ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
尽量使用 Java. util. concurrent 并发类代替自己手写锁。
尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
尽量减少同步的代码块。

说一下 runnable 和 callable 有什么区别?

相同点
都是接口
都可以编写多线程程序
都采用Thread.start()启动线程
主要区别
Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、
FutureTask配合可以用来获取异步执行的结果
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出
异常,可以获取异常信息
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下
执行,如果不调用不会阻塞。

什么是 Future?

在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不
管是继承 thread 类还是实现 runnable 接口,都无法保证获取到之前的执行结果。
通过实现 Callback 接口,并用 Future 可以来接收多线程的执行结果。
Future 表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加
Callback 以便在任务执行成功或失败后作出相应的操作。

什么是 FutureTask ?

FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对
这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时
候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable
和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可
以放入线程池中。

ThreadLocal 是什么 ?有什么用 ?

ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存
放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可
以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。
简单说 ThreadLocal 就是一种以空间换时间的做法,在每个 Thread 里面维护了
一个以开地址法实现的 ThreadLocal.ThreadLocalMap,把数据进行隔离,数据
不共享,自然就没有线程安全方面的问题了。

乐观锁和悲观锁的理解及如何实现,有哪些实现方式?

悲观锁
悲观锁就是持悲观态度的锁。每次去拿数据的时候认为别的线程也会同时修改数据,所以每次在拿数据的时候都会上锁,这样别的线程想拿到这个数据就会阻塞直到它拿到锁。

乐观锁
乐观锁就是持比较乐观态度的锁。就是在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有更新过这个数据。

乐观锁的实现方式:
1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采
取丢弃和再次尝试的策略。
2、java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其
中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失
败,并可以再次尝试。 CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期
原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置
值更新为新值 B。否则处理器不做任何操作。

线程池的优点?使用过哪些线程池 ?

1.复用存在的线程,减少对象创建和销毁的开销
2.可有效对的控制最大并发线程数,提供系统资源利用率
3.提供定时执行,定期执行、单线程、并发控制等。

(1)newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就
是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代
它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
(2)newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程
达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结
束,那么线程池会补充一个新线程。如果希望在服务器上使用线程池,建议使用 newFixedThreadPool
方法来创建线程池,这样能获得更好的性能。
(3) newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要
的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能
的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者
说 JVM)能够创建的最大线程大小。
(4)newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任
务的需求。

线程池的底层工作原理 ?

线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时

(1)如果,线程池中的线程数量 < corePoolSize,则每来一个任务,就创建线程去执行这个任务;

(2)如果,线程池中的线程数量 ≥ corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;

(3)如果,线程池中的线程数量 ≥ corePoolSize,并且队列 workQueue 已满,

但线程池中的线程数量 < maximumPoolSize,则会创建新的线程来处理被添加的任务;

(4)如果,线程池中的线程数量 ≥ maximumPoolSize,则会采取拒绝策略进行处理。

假如核心线程数是5,最大线程数是10,阻塞队列也是10
1)有新任务来的时候,将先使用核心线程执行;
2)当任务数达到5个的时候,第6个任务开始排队;
3)当任务数达到15个的时候,第16个任务将开启新的线程执行,也就是第6个线程
4)当任务数达到20个的时候,线程池满了,如果有第21个任务,将执行拒绝策略

你知道怎么创建线程池吗 ?

创建线程池的方式有多种,这里你只需要答 ThreadPoolExecutor 即可。
ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池
的方式。
ThreadPoolExecutor构造函数重要参数分析
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
maximumPoolSize :线程池中允许存在的工作线程的最大数量
workQueue :当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的
话,任务就会被存放在队列中。
ThreadPoolExecutor 其他常见参数:

  1. keepAliveTime :线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提
    交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会
    被回收销毁;
  2. unit : keepAliveTime 参数的时间单位。
  3. threadFactory :为线程池提供创建新线程的线程工厂
  4. handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略

线程池中 submit() 和 execute() 方法有什么区别 ?

接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的
任务。
返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
异常处理:submit()方便Exception处理

在 Java 中 Executor 和 Executors 的区别 ?

Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展,提供了更多的方法我们能获得任务执
行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor 可以创建自定义线程池。
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使
用 get()方法获取计算的结果。

Sychronized的偏向锁、轻量级锁、重量级锁 ?

  1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就
    可以直接获取到了
  2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个
    线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻
    量级锁底层是通过⾃旋来实现的,并不会阻塞线程
  3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
  4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒
    这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标
    记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运
    ⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。

Sychronized和ReentrantLock的区别 ?

  1. sychronized是⼀个关键字,ReentrantLock是⼀个类
  2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
  3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
  4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
  5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识
    来标识锁的状态
  6. sychronized底层有⼀个锁升级的过程

什么是自旋 ?

很多synchronized里面的代码只是一些很简单的代码,执行速度很快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行非常快,不妨让等待的线程不要被阻塞,而是在synchronized
的边界做忙循环,这就是自旋。如果做了多次忙循环还没有获得锁,再阻塞,这样可能是一种更好的策略。

什么是CAS ?

CAS 是compare and swap 的缩写,就是我们所说的比较并交换

CAS是一种基于锁的操作,而且是乐观锁。在Java中锁分为乐观锁和悲观锁,悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采用了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁与很大的提高。

CAS操作包括三个操作数 内存位置、预期原值和新值。如果内存地址里面的值和A值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,如果在第一轮循环中,A线程获取地址里面的值被B线程修改了,那么A线程需要自旋,到下次循环才有可能有机会执行。

CAS会产生什么问题 ?

1.ABA问题
比如说一个线程ONE从内存位置V中取出A,这时候另外一个线程two也从内存中取出A
,并且two进行了一些操作变为了B,然后two又将数据变为A,这时候线程one进行CAS
操作发现内存中仍然是A 然后one操作成功。尽管线程one的CAS操作成功,但是可能存在隐藏问题。从JDK 1.5 开始JDK的atomic包里面提供了一个AutomicStampadReference
来解决ABA问题。
2.循环时间开销大:
对于资源竞争严重的情况,CAS自旋的概率比较大,从而浪费更多的CPU资源,
效率低于synchronized。
3.只能保证一个共享变量的原则操作:
当对于一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原则操作,但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这各时候可以用锁。

谈谈你对volatile 的理解 ?

什么是volatile

  • volatile是一个Java关键字
  • volatile是Java虚拟机提供的轻量级的同步机制

volatile三大特性

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

volatile如何保证可见性

线程对共享变量的副本做了修改,会立刻刷新最新值到主内存中。
线程对共享变量的副本做了修改,其他其他线程中对这个变量拷贝的副本会时效;
其他线程如果需要对这个共享变量进行读写,必须重新从主内存中加载。
volatile如何禁止指令重排序
为了提高性能,编译器和处理器常常会对指令进行重排序。

那Volatile是怎么保证不会被执行重排序的呢?

内存屏障

Java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。

为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:

图片

需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

图片

图片

谈谈你对AQS的理解 ?

AQS 是 AbustactQueuedSynchronizer 的简称,它是一个 Java 提高的底层同步工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管理这个同步状态。
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广、泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于AQS 的。

常用的并发工具类有哪些?

Semaphore(信号量) : Semaphore翻译过来是信号量的意思,它的作用是控制多个线程对同一个资源的访问线程数量。

CountDownLatch(倒计时器): CountDownLatch是一个倒数的计数器阀门,初始化时阀门关闭,指定计数的数量,当数量倒数减到0时阀门打开,被阻塞线程被唤醒。

CyclicBarrier(循环栅栏):CyclicBarrier是一个可循环的屏障,它允许多个线程在执行完相应的操作后彼此等待共同到达一个point,等所有线程都到达后再继续执行。

说一下 Atomic 的原理?

Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个变量进行操作时,仅有一个线程能成
功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
AtomicInteger 类的部分源码:

// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。
UnSafe 类的objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。
另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值