多线程常见问题

线程的状态有哪些?

线程的生命周期大致分为5个阶段:
NEW(新建):新建一个Thread对象时,此时还没有线程。
RUNNABLE(就绪):调用start方法可以使线程进入就绪状态。
RUNNING(运行):处于就绪状态的线程获得了CPU就可以执行业务,进入运行状态。
BLOCKED(阻塞):处于运行状态的线程如果调用了sleep、wait方法或者竞争锁失败会进入阻塞状态。
TERMINATED(终止):线程正常结束或者意外终止会进入终止状态。
线程的生命周期是比较基础的知识,一般是作为一个切入点有浅入深的问其他问题,在回答这个问题的时候不要干巴巴的只说5个状态单词,要把每个状态是怎么进入的也要讲一下。你如果不讲,面试官就会继续追问:那线程在哪些情况会进入阻塞状态呢?

synchronized锁的升级机制

synchronized是JVM关键字,可以用来修饰代码块、方法(普通方法和静态方法),它是一个可重入的不可打断的锁。
线程竞争synchronized锁其实是对锁对象monitor的竞争,代码块是Object锁对象的monitor,方法是this monitor,静态方法是class monitor。
锁的升级机制:
synchronized在JDK1.5时锁竞争失败后会直接进入阻塞状态,线程上下文之间切换比较消耗CPU性能,在JDK1.5之后对锁进行了优化,引入了无锁,偏向锁,轻量级锁和重量锁。锁的升级过程即是无锁状态到重量级锁状态的变化过程。
锁对象头(Mark Word)有一个偏向标志,无锁状态时为0,当线程获取到锁时会把偏向标志修改为1,并且CAS把对象头中的线程ID修改为自己的线程ID,当下一个线程竞争锁时,发现偏向标志为1,且锁还未释放,则会复制对象头的信息到自己线程的Lock Record中,并且CAS修改对象头中的指针指向自己的LR,如果修改失败则自旋一定次数后(默认10次)升级为重量级锁,此时竞争失败的线程将会挂起。

synchronized和lock的区别?

特点:synchronized是独占可重入锁,是非公平的竞争锁方式。ReentrantLock也是独占可重入锁,但是其可以指定为公平锁,默认是非公平锁。
用法:synchronized可以修饰方法和代码块,不需要显示的加锁和解锁。ReentrantLock修饰代码块,在lock()和unlock()方法中间的代码都是同步代码,需要显示的加锁和解锁,将锁的控制权交给了开发人员。
性能:基于JVM对关键字的支持,单线程下synchronized关键字性能要优于ReentrantLock,但是多线程环境下ReentrantLock性能优于synchronized。
高级特性:获取synchronized锁失败的线程会一直阻塞直到获取到锁,不能中断。ReentrantLock提供了可中断获取锁的方法lockInterruptibly(),而且还提供了获取锁失败不阻塞立即返回的方法tryLock(),如果开发场景中涉及到了高级应用,那就只能选择显示锁Lock了。

两种锁的使用场景?

在多数情况下会优先选择synchronized关键字,在必须要用到高级特性的时候选择Lock锁。另外一点就是要注意synchronized锁升级的不可逆性,并发量谷峰值差别较大的时候优先选择Lock锁。

开发中你是怎么创建线程的?

有4种创建方式,通常使用线程池的方式管理线程,并且线程的参数都是可配置的。

线程池的参数有哪些?

corePoolSize:核心线程数量,即使线程池中的线程空闲也会一直保持此数量的线程,除非设置了allowCoreThreadTimeOut为true。
maximumPoolSize:线程池中允许的最大线程数量。
keepAliveTime:当线程池中的线程数量超过了核心线程的数量,在回收空闲的线程时线程将等待任务的最大时间,超过这个时间还没有任务执行,那么线程将会被回收。
unit:超过核心线程数量的线程空闲时等待任务的时间单位。
workQueue:用于存放提交任务的工作队列,这个队列只存放提交但是未执行的Runnable。
threadFactory:创建线程的工厂。
handler:当线程数量和队列元素都达到最大时,拒绝再次提交任务的策略。

线程池的拒绝策略有哪些?开发中你是怎么选择的?

拒绝策略有4种:直接丢弃,丢弃队列中第一个任务,抛出异常,调用者执行。
前两个开发中一般较少选择,因为不可控。开发中勾勾用过后面两种:抛出异常捕获了记录日志。让主线程去执行保证任务一定被运行。

开发中使用多线程要注意什么?

避免死锁

什么情况下会发生死锁?

交叉锁互相等待肯定会死锁,还有比如内存不足导致线程无法执行,死循环导致的锁无法释放,显示锁开发人员忘记释放锁也会导致死锁。
问到这里可能面试官真的就多嘴一问:你们项目中遇到过死锁的情况吗,线上是怎么排查的?

什么是乐观锁和悲观锁?

乐观锁和悲观锁都是一种思想,有对应的实现。乐观锁主要用于多读的场所,悲观锁主要用于多写的场所。
悲观锁:在修改数据之前先加锁,再对数据进行修改的加锁方式。在数据修改的整个过程中都会加锁。悲观锁又分为读锁和写锁。
乐观锁:在修改数据前不需要加锁,只有在对数据进行修改的时候才会进行检测。可以利用CAS实现乐观锁。
CAS比较并交换,但是会出现ABA的问题,如果不能接受ABA那么在比较的时候可以添加版本号。

volatile关键字的作用?

volatile关键字是一个轻量级锁,它可以保证可见性和有序性,但是不能保证原子性。
此时面试官可能会问:它是如何保证可见性和有序性的,为什么不能保证原子性?
读volatile修饰的变量时,会从主内存中取数据,然后在线程的工作内存中创建变量副本。写volatile修饰的变量时,会对总线lock加锁,此时其他CPU都不能访问到这个变量,当线程将修改后的数据写入主内存并通知其他CPU的数据失效后,对总线解锁。其他线程在后续的过程中因为变量失效不得不从主内存再次获取数据,从而保证了可见性。
有序性就是通过内存屏障实现的。

ThreadLocal原理是什么?

ThreadLocal可以实现线程之间的分离,ThreadLocal修饰的变量,每个线程都会复制一份,因此常用来修饰静态变量。ThreadLocal内部维护了Entry数组,其中key是当前线程,Value是用户存入的变量。
ThreadLocal内部类Entry继承了弱引用,key是weak的引用,一旦GC不管内存是否充足都会被回收,但是value是强引用,会存在null的key指向value,因而造成内存泄漏。因此使用结束需要手动的remove,避免内存泄漏。

AQS的原理是什么?

AQS是JUC工具类的核心,它内部维护了volatile的状态变量state,锁的竞争即是对state的竞争,竞争失败的线程会加入阻塞队列,它是一个先进先出的队列。AQS提供了不同的获取锁和释放锁的操作,包括独占模式(公平和非公平的获取锁方式),共享模式,条件等待模式。线程的node节点对象维护了4个状态:CANCLLED、SIGNAL、CONDITION、PROPAGATE ,决定了线程是否需要挂起和唤醒。

CountDownLatch和CyclicBarrier的区别

  1. CountDownLatch的计数器只能使用一次。而CyclicBarrier的计数器可以使用reset()
  2. 方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次
  3. CyclicBarrier还提供其他有用的方法,比如getNumberWaiting方法可以获得CyclicBarrier阻塞的线程数量。isBroken方法用来知道阻塞的线程是否被中断。比如以下代码执行完之后会返回true
  4. CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。某线程中断CyclicBarrier会抛出异常,避免了所有线程无限等待

场景:如何实现三个线程顺序执行?

AQS实现类的用法一般都是给你描述个场景问你如何实现,比如题目的线程顺序执行,还有一个线程等待多个线程该如何实现,一个系统最多只能100个人登录如何实现?
就是想看看你是否了解Condition、CountDownLatch、CyclicBarrier、Semaphore的用法。
Condition只能由Lock锁创建,一般适用于两个线程之间的等待。
CountDownLatch是AQS共享模式的实现,state状态值表示计数器,await方法会使线程进入阻塞状态直到其他线程调用countdown方法将计数器减为0,CountDownLatch适用于一对多等待的场景。
CyclicBarrier常用来与CountDownLatch作比较,它也可以实现CountDownLatch一对多的等待,CyclicBarrier是一个栅栏,初始化指定屏障个数,所有的线程都到达屏障之后才可以继续执行,会使线程阻塞,且其可以重复使用。
Semaphore是许可证,常用来限制访问的数量,也是AQS共享模式的实现,state状态值用来表示许可证的数量。