多线程面试题

多线程面试题

CAS

CAS(compireAndSwap)比较并交换,它体现了乐观锁的一种思想,在无锁的情况下保证线程操作数据的原子性,它的内部存在3个操作数

1、变量内存值V

2、旧的预期值A

3、准备设置的新值B

当执行CAS指令时,只有当V=A时,才会去执行B更新V的值,否则不会更新

当多个线程同时使用CAS去操作一个变量时,只有一个线程会执行成功,其他线程均会失败,然后会重新尝试或将线程挂起(阻塞)

另外,CAS是一种系统原语,它的执行一定是连续不被中断的,也就不存在并发问题,这样就保证了原子性

CAS虽然能很高效的解决原子操作,但是仍然存在问题

ABA问题 因为CAS只是判断获取值和在操作时这个值之间的时间该没改变来进行操作,当在这个时间内如果有一个操作修改了这个内存变量的值,由A改为B再改为A,这时CAS会认为这个值从来没有变过,但是值其实已经发生了一次改变

循环时间长时开销大 因为底层是自旋锁,当操作迟迟无法完成的时候,会对CPU带来非常大的开销

只能保证一个共享变量的原子操作 当对多个共享变量进行原子操作时,循环CAS就无法保证操作的原子性

为什么不推荐使用Executors创建线程池?

使用Executors创建FixedThreadPool时,构造方法会创建无界阻塞队列LinkedBlockingQueue,使用这个线程池执行任务,如果任务过多就会不断地添加到队列中,任务越多占用的内存就越多,很有可能会造成OOM,不能自定义线程的名字,不利于排查问题,建议直接使用ThreadPoolExecutor来定义线程池,这样可以灵活控制

sychronized和ReentrantLock的区别

sychronized是java关键字,是jvm层面的锁,自动加锁与释放锁,是非公屏锁,锁的是对象

ReentrantLock是jdk提供的一个类,是api层面的锁,需要手动加锁和释放锁,是公平锁或非公平锁

ReentrantLock公平锁和非公平锁
  1. 公平锁加锁时会先检查AQS队列中是否存在线程在排队,如果有现成在排队,则当前线程也进行排队
  2. 非公平锁加锁时不会检查是否有线程在排队,而是直接竞争锁
  3. 无论公平锁和非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程
何为AQS,AQS如何实现可重入锁
  1. AQS是一个java线程同步的框架,是jdk中很多锁工具的核心实现框架
  2. 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同场景下,有不同的意义。
  3. 在可重入锁这个场景下,state就用来表示加锁的次数。0标识无所,每加一次锁,state就加1,释放锁就减1
ThreadLocal有哪些应用场景?底层如何实现
  1. ThreadLocal是java中提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
  2. 底层是通过ThreadLocalMap实现的,每个Thread对象中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,value为需要缓存的值。
  3. 可能会造成内存泄漏,使用完记得调用remove方法移除
  4. 应用场景:登录时存储当前用户信息
线程池中提交一个任务的流程是怎样的?

提交线程–》判断线程数是否超过核心线程?如果没有超过就创建新线程,如果超过了就尝试将runable加入到workQueue中等待被执行,但是如果workQueue也满了,就需要判断线程数是否超过了最大线程数,如果没超过则创建新线程,如果超过了就拒绝当前任务

线程池有几种状态?如何变化?
状态详情
RUNNING会接收新任务并且会处理队列中的任务
SHUTDOWN不会接收新任务并且会处理队列中的任务,任务处理完后会中断所有线程,关闭线程池
STOP不会接收新任务并且不会处理队列中的任务,并且会直接中断所有线程,关闭线程池
TIDYING所有线程都停止了之后,线程池的状态就会转为TIDYING,一旦达到此状态,就会调用线程池的terminated()
TERMINATEDterminated()执行完之后就会转变为TERMINATED
转变前转变后转变条件
RUNNINGSHUTDOWN手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown()
RUNNINGSTOP手动调用shutdown()触发
SHUTDOWNSTOP手动调用shutdown()紧接着调用shutdownNow()触发
SHUTDOWNTIDYING线程池所有线程都停止后自动触发
STOPTIDYING线程池所有线程都停止后自动触发
TIDYINGTERMINATED线程池自动调用terminated()后触发
如何优雅地停止线程?

调用interrupt()方法

如何设置线程池的核心线程数、最大线程数?

对于cpu密集型任务,设置核心线程数为cpu核心数+1

对应IO密集型任务,设置核心线程数为cpu核心数*2

理解java并发中的可见性

有一个共享的变量i,当线程A读取变量i时,会从内存中读取数据,并缓存一份在cpu1内部的高速缓存中,然后线程1修改i,改为i=2,但是还没有回写到内存,此时线程B也来读取i,那么也会从内存中读取,读取到的仍然为1,此时就出现了可见性的问题。

在java中,可以用volatile关键字来保证变量的可见性,对于加了volatile的,线程在读取该变量时会直接从内存中读取,修改该变量时会同时修改cpu高速缓存和内存中的值。

理解java并发中的有序性

java并发有序性指的是多个线程执行的指令和操作,按照开发者编写程序的顺序或者预定的顺序进行执行。多线程并发执行时,可能会发生指令的重排,导致程序的执行顺序与预期不一致,从而出现数据竞争和安全问题。

我们可以通过锁机制或者volatile来保证有序性

何为死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁

例如,两个食客来西餐厅吃牛排,此时餐厅还剩一把刀和一把叉子,只有同时拿到刀叉才能顺利进餐,此时食客A拿到了刀,食客B拿到了叉子,但是他们两个都不舍得放弃手中的餐具,又拿不到另一把需要的餐具,此时谁都吃不到牛排。

java中如何避免死锁
  1. 在开发过程中注意加锁的顺序,保证每个线程按同样的顺序进行加锁,如线程1要先加A锁再加B锁,线程2也要按同样的加锁顺序加锁

  2. 要注意加锁时限,可以针对锁设置一个超时时间如ReentrantLock 中使用lock.tryLock(100, TimeUnit.MILLISECONDS)

  3. 注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

并发、并行、串行的区别

并发:在同一时刻,有多个指令在单个CPU上交替执行

并行:在同一时刻,有多个指令在多个CPU上同时执行

串行:一个任务执行完,才能执行下一个任务

何为守护线程

当非守护线程执行完毕之后,守护线程就没有执行下去的必要了,就会陆陆续续结束,例如QQ聊天是非守护线程,文件传输时守护线程,如果聊天框关闭了,那么文件传输就结束了

如何理解线程安全

线程安全指的是,我们写的某段代码,在多个线程同时执行这段代码时,不会产生混乱,依然能够得到正常的结果,不如i++,i的初始值为0,那么两个线程同时来执行这行代码,如果代码时线程安全的,那么最终的结果应该是一个线程的结果为1,一个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码时线程不安全的

线程池的底层工作原理

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

  1. 如果此时线程池中的线程数量小于核心线程数,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务
  2. 如果此时线程池中的线程数量等于核心线程数,但是缓冲队列workQueue未满,那么任务被放入缓冲队列
  3. 如果此时线程池中的线程数量大于等于核心线程数,缓冲队列workQueue满,并且线程池中的数量小于最大线程数,创建新的线程来处理被添加的任务
  4. 如果此时线程池中的线程数量大于核心线程数,缓冲队列workQueue满,并且线程池中的线程数量等于最大线程数,那么通过hander所指定的策略处理被添加的任务
  5. 当线程池中的线程数量大于核心线程数,如果某线程空闲时间超过keepAtiveTime,线程将被终止,动态调整线程池中的线程数
Sychronized锁升级
  1. 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程id,该线程下次如果又来获取该锁就可以直接获取到了
  2. 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开,轻量级锁是通过自旋来实现的,并不会阻塞线程
  3. 如果自旋的次数过多,仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞,把没有获得到锁的线程放到阻塞队列中去
何为自旋锁

自旋锁:在线程获取锁的过程中,不会去阻塞线程,而是一直占用CPU时间片,通过不停地尝试去获取锁,如果获取失败就再次尝试,直到成功为止。因为阻塞和唤醒线程这两个步骤都是需要操作系统去调度的,比较浪费时间。如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,减少消耗的时间。

ReentrantLock的lock()方法是阻塞加锁,tryLock()方法是自旋锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值