多线程面试题
1.解释下多线程
线程,线程就是程序的执行路径,或者可以说是程序的控制单元
一个进程可能包含一个或多个进程,当一个进程存在多条执行路径时,就可以将该执行方式称为多线程,
线程的执行方大致可分为就绪(wait),执行(run),阻塞(block)三个状态,多个线程在运行中相互抢夺资源,造成线程在上述的三个状态之间不断地相互转换,
2.创建线程的三种方法
(1)继承Thread类,重写父类run()方法.
(2)实现runnable接口
(3)使用ExcutorService,Callable,Future实现有返回结果的多线程(JDK1.5以后)
3.多线程同步机制
(1)在需要同步的方法的方法签名中加入synchronized关键字
(2)在使用synchronized块对需要进行同步的代码段进行同步
(3)使用JDK5中提供的Java.util.concurrent.lock包中的Lock对象
4.线程的几种可用状态(生命周期)
就绪(Runnable):线程准备运行,不一定立马就能开始执行.
运行中(Running):线程正在执行线程的代码.
等待中(Waiting):线程处于阻塞的状态,等待外部的处理结果.
睡眠中(Sleeping):线程被强制睡眠.
I/O阻塞(Blocked on I/O):等待I/O操作完成,
同步阻塞(Blocked on Synchronization):等待获取锁.
死亡(Dead):线程完成了执行.
5.线程锁对象详解.
当多个线程同时访问共享数据时,可能导致数据不同步,甚至错误!
适用于密集型操作,需要资源保持同步,需要用到线程锁
优缺点:
优点:保证资源同步
缺点:有等待肯定会慢
6.同步方法的实现方式
(1)同步方法:有synchronized关键字修饰的方法
(2)同步代码块:有synchronized关键字修饰的语句块
(3)使用特殊域变量(volatile)实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
(4)使用重入锁实现线程同步
(5)使用局部变量threadlocal实现线程同步
(6)使用阻塞队列实现线程同步
(7)使用原子变量实现线程同步
7.什么是死锁(deadlock)如何避免死锁?
两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁,
避免死锁的方式就是:指定获取锁的顺序,并强制线程按照指定的顺序获取锁.
8.sleep方法和wait方法的区别?
来自不同的类:wait()方法是Object类的方法,sleep方法是Thread类的方法.
对于锁的占用情况不同:sleep方法没有释放锁,而wait方法释放了锁
使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep()可以在任何地方使用
唤醒方式:sleep()方法的线程通常是睡眠一定时间后,自动醒来,调用wait()方法,必须采用notify()或者notifyAll()方法唤醒
9.线程局部变量ThreadLocal
ThreadLocal的作用和目的:
用于实现线程内的数据共享,相同的程序代码下多个模块在同一个线程中运行时要共享一份数据
ThreadLocal的应用场景:
订单处理包含一系列操作
银行转账包含一系列操作
同一段代码被不同的线程调用运行时
10.什么是守护线程
守护线程(即daemon thread),是个服务线程,服务于其他的用户线程
(1).守护线程,比如垃圾回收线程,就是最典型的守护线程,
(2).用户线程,就是应用程序里的自定义线程
用户自定义线程
(1).应用程序里的线程,一般都是用户自定义线程
(2).用户也可以自定义守护线程,只需要调用Thread类的设置方法设置一下即可.
11.synchronized和volatile比较
(1)volatile不需要加锁,比synchronized更轻便,不会阻塞线程,
(2)synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性
12.乐观锁和悲观锁的理解及如何实现?原理和应用场景?
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会去修改,所以每次在拿数据的时候都会上锁,
这样别人想拿这个数据就会阻塞直到它拿到锁
悲观锁,先获取锁,在进行业务操作,必须在事务中使用.
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁.
但是在更新的时候会判断一下再次期间别人有没有去更新这个数据,可以使用版本号等机制
乐观锁,先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过,
并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本更新.
13.为什么要用线程池?
减少了创建线程和销毁线程的次数,每个工程线程都可以被重复利用,可执行多个任务.
14.3个方法代表了Servlet的生命周期.
(1)init方法:负责初始化Servlet对象.
(2)service方法:负责响应客户的请求.
(3)destory方法:当Servlet对象退出生命周期时,负责释放占用的资源.
15.线程的run()和start()有什么区别?
run()调用Thread中定义的方法
start()启动线程
16.如何唤醒一个阻塞的线程?
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,
并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关.
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,
而是让它们竞争,只有获得锁的线程才能进入就绪状态.
17.在Java程序中怎么保证多线程的运行安全?
使用安全类,比如java.util.concurrent下的类,使用原子类AtomicInteger
使用自动锁 synchronized.
使用自动锁 Lock
18.线程之间如何通信及线程之间如何同步
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll,await/signal/signalAll进行唤起和等待
19.说说自己是怎么使用synchronized关键字,在项目中用到了吗?
synchronized关键字可以用在如下三个地方:
修饰实例方法
修饰静态方法
修饰代码块
项目开发过程中很少使用synchronized关键字修饰,因为效率比较低,会阻塞,对于互联网项目来说不太合适,一般在单例模式中使用synchronized实现双重检测锁
20.单例模式了解吗?解释一下双重检验锁方式实现单例模式
饿汉,懒汉,双重检查锁.
21.新建T1,T2,T3三个线程,如何保证他们按顺序执行?
用join方法
join()方法:谁调用这个方法,就让调用方的线程进入阻塞状态,等待我执行完毕后,再往下执行.
22.形成死锁的四个必要条件是什么?
互斥条件:在一段时间内某资源只由一个进程占用,如果此时还有其他进程请求资源,就只能等待,直至占有资源的进程用毕解放.
占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程阻塞,但又对自己已获得的其他资源保持不放.
不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来.
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系(比如一个进程集合,A在等B,B在等C,C在等A)
23.线程B怎么知道线程A修改了变量?
volatile修饰变量
synchronized修饰修改变量的方法
wait/notify
while 轮询探测,例如CAS
24.创建线程池的参数有哪些
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超过的线程会在队列中等待.
newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行,
newSingleThreadExecutor创建一个定长线程池,他只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行
25.如何创建线程池
使用new的方式
使用Excutors工具类创建
26.线程池的执行流程
提交一个任务到线程池中,线程池的处理流程如下:
(1)判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务,如果核心线程都在执行任务,则进入下个流程
(2)线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务储存在这个工作队列里,如果工作队列满了,则进入下个流程
(3)判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,如果已经满了,则交给饱和策略来处理这个任务.
27.如何合理分配线程池大小?
CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务
IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数.
28.线程池启动线程submit()和execute()方法有什么不同?
execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多.
submit返回一个Future对象,如果想知道线程结果就是用submit提交.
而且它能在主线程中通过Future的get方法捕获线程中的异常
29.如果你提交任务时,线程队列已满,这时会发生什么?
有两种可能:
(1)如果使用的是无界队列LInkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务队列到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务.
(2)如果使用的是有界队列比如ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue中,
ArrayBlockingQueue满了会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,
ArrayBlockingQueue继续满,那么则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolity
30.多线程中synchronized锁升级的原理是什么?
在Java中,锁共有4中状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁,和重量级锁状态,这几个状态会随着竞争情况逐渐升级,所可以升级但不能降级
synchronized锁升级原理:在锁对象的对象头里面有一个threadid字段,
在第一次访问的时候threadid为空,jvm让其持有偏向锁,并将threadid设置为其线程id,
再次进入的时候会先判断threadid是否与其线程id一致,如果一致则可以直接使用此对象,
如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,
执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized锁的升级
31.什么是CAS?
CAS是一种基于锁的操作,而且是乐观锁,.在Java中锁分为乐观锁和悲观锁
CAS是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋到下次循环才有可能机会执行
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止,可以理解成一个无阻塞多线程争抢资源的模型
32.CAS会产生什么问题?
ABA问题:从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题
循环时间长开销大:对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized.
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对于多个共享变量的操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁.
33.什么是偏向锁?
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,
如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的.
减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁.
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁.
34.什么时轻量级锁.什么是重量级锁?
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,
当第二个线程加入锁争用的时候,轻量级锁就会升级为重量级锁,
重量级锁是synchronized,是Java虚拟机中最为基础的锁实现.
在这种状态下,Java虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程,
35.什么是自旋锁?自旋锁存在什么问题?
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,
此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题
既然synchronized里面的代码执行的非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋.
存在什么问题:
如果某个线程持有锁的时间过长,就会导致其他等待获取锁的线程进入循环等待,消耗CPU,使用不当会造成CPU使用率极高,
上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁,不公平的锁就会存在"线程饥饿"问题,
36.synchronized和Lock有什么区别?
(1)synchronized是Java内置关键字,在JVM层面.Lock是个Java类
(2)synchronized可以给类,方法.代码块加锁.而Lock只能给代码块加锁
(3)synchronized不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁.而Lock需要自己加锁和释放锁,如果使用不当没有unLock()去释放锁就会造成死锁,
通过Lock可以知道有没有成功获取锁,而synchronized却无法办到.
37.volatile关键字的作用
Java提供了volatile关键字来保证可见性和禁止指令重排,确保一个线程的修改能对其他线程是可见的.
它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新的值.
volatile可以提供线程间的可见性,但是不能保证操作的原子性.
38.ThreadLocal的底层原理
ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻,任意方法中去获取缓存的数据,
ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
(如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不被回收,从而出现内存泄漏,解决办法是,在使用ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象)
39.项目中哪里使用的ThreadLocal
(1)经历的项目中,一般用ThreadLocal保存用户登录信息.在项目中提供一个过滤器,拦截到所有的请求,获取请求中的UserId头,构建User对象保存到ThreadLocal
(2)在ThreadLocal中保存一些当前请求的响应信息,在返回的时候还是通过过滤器把响应信息从ThreadLocal中取出来,返回给客户端.
(3)我们锁使用的很多框架内容也使用了ThreadLocal机制,例如Spring的事务控制就会将连接放入到ThreadLocal,Seata的分布式事务控制也会将全局事务XID存入到ThreadLocal
40.使用ThreadLocal可能会产生什么问题?怎么解决?
可能会产生内存泄漏问题:因为当ThreadLocal对象使用完之后,应该要把设置的key,value也就是Entry对象进行回收,但线程池的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象,
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)