JUC多线程常见面试题汇总

1.并行和并发有什么区别?

并行是指多个处理器同时执行多个任务,每个核心实际上可以在同一时间独立地执行不同的任务。

并发是指系统有处理多个任务的能力,但是任意时刻只有一个任务在执行。在单核处理器上,多个任务是通过时间片轮转的方式实现的。但这种切换非常快,给人感觉是在同时执行。

2.线程和进程的区别?

进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务,不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间

线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

3.java中创建线程有哪些方式?

在java中一共有四种常见的创建方式,分别是:继承Thread类、实现Runnable接口、实现Callable接口、线程池创建线程。通常情况下,我们项目中都会采用线程池的方式创建线程。

4.Runnable和Callable两个接口创建线程有什么不同?

最主要的是两个线程一个是有返回值,一个是没有返回值的。

Runnable 接口run方法无返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

还有一个就是,他们异常处理也不一样。Runnable接口run方法只能抛出运行时异常,也无法捕获处理;Callable接口call方法允许抛出异常,可以获取异常信息

在实际开发中,如果需要拿到执行的结果,需要使用Callalbe接口创建线程,调用FutureTask.get()得到可以得到返回值,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

5.线程包括哪些状态,状态之间是如何变化的?

在JDK中的Thread类中的枚举State里面定义了6种线程的状态分别是:新建、可运行、终结、阻塞、等待和有时限等待六种。

新建:当一个线程对象被创建,但还未调用start方法时处于新建状态,调用了start方法,就会由新建进入可运行状态。如果线程内代码已经执行完毕,由可运行进入终结状态。当然这些是一个线程正常执行情况。

阻塞:如果线程获取锁失败后,由可运行进入Monitor的阻塞队列阻塞,只有当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态。

等待:如果线程获取锁成功后,但由于条件不满足,调用了wait()方法,此时从可运行状态释放锁等待状态,当其它持锁线程调用notify()或notifyAll()方法,会恢复为可运行状态。

有时限等待:还有一种情况是调用sleep(long)方法也会从可运行状态进入有时限等待状态,不需要主动唤醒,超时时间到自然恢复为可运行状态。

6.线程中的wait和sleep方法有什么不同?

它们两个的相同点是都可以让当前线程暂时放弃CPU的使用权,进入阻塞状态。

不同点主要有三个方面:

第一:方法归属不同sleep(long) 是 Thread 的静态方法。而 wait()是 Object 的成员方法,每个对象都有。

第二:线程醒来时机不同。线程执行 sleep(long) 会在等待相应毫秒后醒来,而 wait() 需要被notify唤醒,wait() 如果不唤醒就一直等下去

第三:锁特性不同。wait 方法的调用必须先获取wait对象的锁,而 sleep 则不用。wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(相当于我放弃 cpu,你们可以用);而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(相当于我放弃 cpu,你们也用不了)。

7.新建T1、T2、T3三个线程,如何保证线程按顺序执行?

在多线程中有多种方法让线程按特定顺序执行,可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。

比如说:使用join方法,T3调用T2,T2调用T1,这样就能确保T1就会先完成而T3最后完成。

8.线程的run()和start()有什么不同?

start方法用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。run方法封装了要被线程执行的代码,可以被调用多次。

9.那如何停止一个正在运行的线程呢?

有三种方式可以停止线程

第一:可以使用退出标志,使线程正常退出,也就是当run方法完成后线程终止,一般我们加一个标记。

第二:可以使用线程的stop方法强行终止,不过一般不推荐,这个方法已作废

第三:可以使用线程的interrupt方法中断线程,内部其实也是使用中断标志来中断线程。

我们项目中使用的话,建议使用第一种或第三种方式中断线程。

10.synchronized关键字的底层原理?

synchronized 底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放锁之前,其他线程是不能或得到锁的。synchronized 属于悲观锁。

monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。monitor内部维护了三个变量:

WaitSet:保存处于Waiting状态的线程

EntryList:保存处于Blocked状态的线程

Owner:持有锁的线程

只有一个线程获取到的标志就是在monitor中设置成功了Owner,一个monitor中只能有一个Owner。在上锁的过程中,如果有其他线程也来抢锁,则进入EntryList 进行阻塞,当获得锁的线程执行完了,释放了锁,就会唤醒EntryList 中等待的线程竞争锁,竞争的时候是非公平的。

11.关于synchronized 的锁升级的情况了解吗?

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。

轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。

偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id就可以,一旦锁发生了竞争,都会升级为重量级锁。

12.ReentrantLock的使用方式和底层原理?

synchronized在高并发量的情况下,性能不高,所以在高并发下,我们可以采用ReentrantLock来加锁。

使用方式:ReentrantLock是一个可重入锁:调用lock方法获取了锁之后,再次调用lock,是不会再阻塞,内部直接增加重入次数就行了,标识这个线程已经重复获取一把锁而不需要等待锁的释放。ReentrantLock是属于juc包下的类,属于api层面的锁,跟synchronized一样,都是悲观锁。通过lock()用来获取锁,unlock()释放锁。

底层实现原理:主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高。

13.介绍一下CAS和AQS?

CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。CAS使用到的地方很多:比如AQS框架、AtomicXXX类。在操作共享变量的时候使用的自旋锁,效率上更高一些。CAS的底层是调用的Unsafe类中的方法。

AQS:其实就是一个jdk提供的类AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。内部有一个state属性来表示资源的状态,默认state等于0,表示没有获取锁,state等于1的时候才标明获取到了锁。通过cas机制设置state状态。

在它的内部还提供了基于FIFO的等待队列,是一个双向列表,其中tail指向队列最后一个元素,head指向队列中最久的一个元素。ReentrantLock底层的实现就是一个AQS。

14.synchronized和Lock有什么区别 ?

主要有三个方面不太一样

第一,语法层面:synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现,退出同步代码块锁会自动释放;Lock 是接口,源码由 jdk 提供,用 java 语言实现,需要手动调用unlock方法释放锁。

第二,功能层面:二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能。Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可中断、可超时、多条件变量,同时Lock 可以实现不同的场景,如 ReentrantLock,ReentrantReadWriteLock

第三,性能层面:在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能较好。在竞争激烈时,Lock 的实现通常会提供更好的性能

统合来看,需要根据不同的场景来选择不同的锁的使用。

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

一个线程需要同时获取多把锁,这时就容易发生死锁,举个例子来说:t1 线程获得A对象锁,接下来想获取B对象的锁;t2 线程获得B对象锁,接下来想获取A对象的锁。这个时候t1线程和t2线程都在互相等待对方的锁,就产生了死锁

16.出现了死锁,如何进行死锁诊断?

需要通过jdk自动的工具解决:

我们可以先通过jps来查看当前java程序运行的进程id;

然后通过jstack来查看这个进程id,就能展示出来死锁的问题,并且可以定位代码的具体行号范围,我们再去找到对应的代码进行排查就行了。

17.谈谈你对volatile的理解

volatile 是一个关键字,可以修饰类的成员变量、类的静态成员变量,主要有两个功能:

第一:保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。

第二:禁止进行指令重排序,可以保证代码执行有序性。底层实现原理是,添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

18.线程池的种类有哪些?

在jdk中默认提供了4中方式创建线程池:

第一个是:newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

第二个是:newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

第三个是:newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

第四个是:newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

19.线程池的核心参数有哪些?

在线程池中一共有7个核心参数:

  1. corePoolSize 核心线程数目 - 池中会保留的最多线程数
  2. maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
  3. keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
  4. unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
  5. workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
  6. threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
  7. handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略 在拒绝策略中又有4中拒绝策略 当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。

20.线程池的执行原理知道吗?

首先判断线程池里的核心线程是否都在执行任务,如果不是则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给拒绝策略来处理这个任务。

21.为什么不建议使用Executors创建线程池呢?

主要原因是如果使用Executors创建线程池的话,它允许的请求队列默认长度是Integer.MAX_VALUE,这样的话,有可能导致堆积大量的请求,从而导致OOM(内存溢出)。

所以,我们一般推荐使用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数,避免资源的耗尽。

22.如何控制某一个方法允许并发访问线程的数量?

在jdk中提供了一个Semaphore类(信号量),它提供了两个方法,semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了。

第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1。

23.如何保证Java程序在多线程的情况下执行安全呢?

jdk中也提供了很多的类帮助我们解决多线程安全的问题,比如:JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题,synchronized、volatile、LOCK,可以解决可见性问题,Happens-Before 规则可以解决有序性问题

24.你在项目中哪里用了多线程?

用户购买多种类型的车座时使用了多线程,例如同一订单购买了一等座和二等座。最初的逻辑是串行的,即先执行完一等座的选座逻辑,再执行二等座的选座逻辑。为了提高执行效率和系统吞吐量,引入了线程池实现并行处理。

多线程应用的场景如下:

  • 当用户在一个订单中选择了多种类型的座位时,例如同时购买一等座和二等座,使用线程池并行处理选座逻辑。这样可以提高选座的效率。
  • 引入动态可监控线程池,通过中间件定义的增强后的动态线程池帮助完成并行业务。每种类型座位的选座逻辑可以分配给不同的线程并行执行,从而加快整个订单处理的速度。
  • 为了解决线程池执行是异步的,确保在购票流程结束前完成所有座位分配逻辑,需要等待线程池执行的结果后再继续后续的提交订单逻辑。

通过多线程的应用,项目中的选座逻辑从串行改为并行,提升了接口响应时间,并在选择多个座位时不会带来额外的消耗。

25.谈谈你对ThreadLocal的理解

ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享。

26.ThreadLocal的底层原理实现

在ThreadLocal内部维护了一个ThreadLocalMap类型的成员变量,用来存储资源对象。

当我们调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中。

当调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值。

当调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值。

27.关于ThreadLocal会导致内存溢出这个事情,了解吗?

是因为ThreadLocalMap中的key被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。

在使用ThreadLocal时都把它作为静态变量(即强引用),因此无法被动依赖GC回收,建议主动的remove释放key,这样就能避免内存溢出。

  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值