多线程面试题
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公平锁和非公平锁
- 公平锁加锁时会先检查AQS队列中是否存在线程在排队,如果有现成在排队,则当前线程也进行排队
- 非公平锁加锁时不会检查是否有线程在排队,而是直接竞争锁
- 无论公平锁和非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程
何为AQS,AQS如何实现可重入锁
- AQS是一个java线程同步的框架,是jdk中很多锁工具的核心实现框架
- 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同场景下,有不同的意义。
- 在可重入锁这个场景下,state就用来表示加锁的次数。0标识无所,每加一次锁,state就加1,释放锁就减1
ThreadLocal有哪些应用场景?底层如何实现
- ThreadLocal是java中提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
- 底层是通过ThreadLocalMap实现的,每个Thread对象中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,value为需要缓存的值。
- 可能会造成内存泄漏,使用完记得调用remove方法移除
- 应用场景:登录时存储当前用户信息
线程池中提交一个任务的流程是怎样的?
提交线程–》判断线程数是否超过核心线程?如果没有超过就创建新线程,如果超过了就尝试将runable加入到workQueue中等待被执行,但是如果workQueue也满了,就需要判断线程数是否超过了最大线程数,如果没超过则创建新线程,如果超过了就拒绝当前任务
线程池有几种状态?如何变化?
状态 | 详情 |
---|---|
RUNNING | 会接收新任务并且会处理队列中的任务 |
SHUTDOWN | 不会接收新任务并且会处理队列中的任务,任务处理完后会中断所有线程,关闭线程池 |
STOP | 不会接收新任务并且不会处理队列中的任务,并且会直接中断所有线程,关闭线程池 |
TIDYING | 所有线程都停止了之后,线程池的状态就会转为TIDYING,一旦达到此状态,就会调用线程池的terminated() |
TERMINATED | terminated()执行完之后就会转变为TERMINATED |
转变前 | 转变后 | 转变条件 |
---|---|---|
RUNNING | SHUTDOWN | 手动调用shutdown()触发,或者线程池对象GC时会调用finalize()从而调用shutdown() |
RUNNING | STOP | 手动调用shutdown()触发 |
SHUTDOWN | STOP | 手动调用shutdown()紧接着调用shutdownNow()触发 |
SHUTDOWN | TIDYING | 线程池所有线程都停止后自动触发 |
STOP | TIDYING | 线程池所有线程都停止后自动触发 |
TIDYING | TERMINATED | 线程池自动调用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要先加A锁再加B锁,线程2也要按同样的加锁顺序加锁
-
要注意加锁时限,可以针对锁设置一个超时时间如ReentrantLock 中使用lock.tryLock(100, TimeUnit.MILLISECONDS)
-
注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
并发、并行、串行的区别
并发:在同一时刻,有多个指令在单个CPU上交替执行
并行:在同一时刻,有多个指令在多个CPU上同时执行
串行:一个任务执行完,才能执行下一个任务
何为守护线程
当非守护线程执行完毕之后,守护线程就没有执行下去的必要了,就会陆陆续续结束,例如QQ聊天是非守护线程,文件传输时守护线程,如果聊天框关闭了,那么文件传输就结束了
如何理解线程安全
线程安全指的是,我们写的某段代码,在多个线程同时执行这段代码时,不会产生混乱,依然能够得到正常的结果,不如i++,i的初始值为0,那么两个线程同时来执行这行代码,如果代码时线程安全的,那么最终的结果应该是一个线程的结果为1,一个线程的结果为2,如果出现了两个线程的结果都为1,则表示这段代码时线程不安全的
线程池的底层工作原理
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
- 如果此时线程池中的线程数量小于核心线程数,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务
- 如果此时线程池中的线程数量等于核心线程数,但是缓冲队列workQueue未满,那么任务被放入缓冲队列
- 如果此时线程池中的线程数量大于等于核心线程数,缓冲队列workQueue满,并且线程池中的数量小于最大线程数,创建新的线程来处理被添加的任务
- 如果此时线程池中的线程数量大于核心线程数,缓冲队列workQueue满,并且线程池中的线程数量等于最大线程数,那么通过hander所指定的策略处理被添加的任务
- 当线程池中的线程数量大于核心线程数,如果某线程空闲时间超过keepAtiveTime,线程将被终止,动态调整线程池中的线程数
Sychronized锁升级
- 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程id,该线程下次如果又来获取该锁就可以直接获取到了
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开,轻量级锁是通过自旋来实现的,并不会阻塞线程
- 如果自旋的次数过多,仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞,把没有获得到锁的线程放到阻塞队列中去
何为自旋锁
自旋锁:在线程获取锁的过程中,不会去阻塞线程,而是一直占用CPU时间片,通过不停地尝试去获取锁,如果获取失败就再次尝试,直到成功为止。因为阻塞和唤醒线程这两个步骤都是需要操作系统去调度的,比较浪费时间。如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,减少消耗的时间。
ReentrantLock的lock()方法是阻塞加锁,tryLock()方法是自旋锁