面试系列之java多线程
1.线程池的优点?
a. 线程是稀缺资源,线程的创建和消费是很耗资源的,使用线程池可以减少线程创建和销毁的次数,使得线程池中的线程可以重复使用。
b. 用线程池可以有效控制系统中的线程数(最大线程数,拒绝策略等),防止因为线程数量过,消耗过多导致服务器奔溃。
2.创建线程池有哪几种方式?
a.ThreadPoolExecutor
b.ScheduledThreadPoolExecutor在a方式的基础上增强支持定时任务的特性
c.ForkJoinPool 可以看做是ThreadPoolExecutor的一个补充,主要用于实现"分而治之"的算法,比较适合计算密集型的任务。
3.线程的生命周期
a.创建线程,调用start方法进入可运行状态
b.可运行状态获取到时间片进入的运行状态
c.正常运行结束或运行过程中出了异常退出线程结束
d.运行状态中如果时间片用完或出让时间片(yield)又回到可运行状态
e.运行状态休眠(sleep)或等待另外一个线程(join)线程进入阻塞状态,此时当前线程并不会释放锁
f.不释放锁的阻塞状态,休眠时间到或者另外一个线程结束又进入到可运行状态
g.运行状态的线程调用wait方法进入到等待队列状态
h.处于等待队列状态中的线程被唤醒(notify或notify all)会处于竞争锁的状态,竞争不到继续待在等待队列中,竞争到了处于可运行状态
大体如下:
![](https://img.haomeiwen.com/i6292941/d7e54a28d36099ab.png)
4.什么是僵死进程?
僵死进程是指:当子进程结束退出的时候,父进程并未对其发出的信号做任何处理,导致子进程处于僵死状态等待父进程对其进行收尸,这个状态下的子进程就是僵死进程。
5.什么是线程安全?
线程安全就是多个线程在执行的过程中并不会产生共享资源的冲突问题就是线程安全。反之多个线程在执行过程中对主内存中的共享变量同时操作就会产生线程安全问题。
6.如何实现线程安全?
a.悲观锁,悲观锁的方式主要有临界区互斥包括Synchorized,ReentrantLock,信号量semaphore一次支持多个个线程访问。
b.乐观锁,乐观锁主要是CAS(Compare And Swap)
c.可重入代码,说白了就是不依赖堆上的共享变量,做到代码可重入执行
d.ThreadLocal,用threadLocal线程本地变量,每个线程自己维护一份自己的本地线程变量,单独进行不受其他线程影响
7.创建线程池有哪些核心参数?
先看下创建线程池的主要方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize:核心线程数量大小
maximumPoolSize:最大线程数量大小
keepAliveTime:线程空闲后的存活时长
workQueue:线程队列
handler:线程队列任务满了之后的拒绝队列
他们之间的主要关系:
a.当线程池中的线程数量小于corePoolSize的时候则会创建线程,并处理请求
b.当线程池中的线程数量大等于corePoolSize的时候,新的任务会放到workQueue中,等核心线程数量空闲之后就会继续从workQueue取出任务来执行
c.当核心线程处理不过来,workQueue队列满了之后,线程池会继续创建线程,数量最多不超过maximumPoolSize,来继续处理队列中的任务
d.当队列满了之后,线程数也达到maximumPoolSize还是处理不过来,此时就会用RejectedExecutionHandler中的拒绝策略来抛弃一些任务
8.如何合理分配线程池大小?
常见的任务一般有IO密集型任务,CPU密集型任务和混合型任务,不同的任务类型分配的大小也有所区别。
a.CPU密集型尽量使用较小的线程池,一般建议是CPU核心数+1,充分发挥cpu的能力,不然线程数太多也是处于等待状态,因为其他线程还没出让CPU
b.IO密集型尽量使用大一点的线程池,一般是CPU核心数*2,更多的线程减少每个线程的IO减轻每个线程的IO压力
c.混合型就要一般在业务上判断是CPU密集型或是IO密集型可以创建不同的线程池来处理
9.说说Volatile的原理和使用场景
volatile修饰的变量,在做写操作时,JVM会发送一个lock前缀的指令,该指令就是保证线程中工作区缓存的结果同步到系统主内存中。这里的lock前缀指令其实就是一个内存屏障的作用,防止指令重排序,屏障前的指令不会在屏障后执行,屏障后的指令不会跑到屏障前执行。说白了就是保证在变量写操作的时候屏障前的指令全部执行完,并将该变量的结果立即同步到系统主内存。
使用场景主要有:单例模式,保证全局可见全局唯一。计数器,计数变量全局唯一可见保证有序。状态位,初始化某个状态值或其他全局性的标识。
10.说说threadLocal的原理和使用场景
threadLocal主要是维护线程本地的变量不受其他线程的干扰,如果是共享变量依赖会有线程安全问题。所以threadLocal主要是维护线程本地变量,内部维护一个Map,key是ThreadLocal本身,value是各线程自己需要的值。
使用场景有,典型的session维护,或者数据库链接维护等。
11.ThreadLocal什么时候会出现内存溢出?
ThreadLocal是维护在Thread下面的,所以只要线程一直在的话ThreadLocal中的变量就会一直持有,这时候是有可能内存溢出的,比如线程池中的线程一直没有销毁,线程一直在,那么线程中维护的ThreadLocal就会一直持有线程的变量。这个时候就要看线程池是怎么实现的有没有去销毁空闲的线程。其他在没有线程池的情况下一般是不会发生内存溢出的,因为在线程退出的时候ThreadLocal的exist方法会做一些重置的工作。
12.说说synchronized和volatile的区别?
a.在使用上volatile只能修饰变量,而synchronized可以修饰变量,方法和类。
b.在达到的效果上volatile修饰的变量主要是针对可见性上而言,多个线程之间可见并且是最新的,synchronized修饰的变量不但能保证可见性还能保证原子性
c.在底层原来上volatile主要是jvm发送lock前缀指令将最新的数据立即刷新到内存,synchronized是通过竞争锁造成线程阻塞的方式保证临界区只有一个线程执行的方式来保证共享变量的安全
d.volatile修饰的变量不会被编译器优化,禁止指令重排而synchronized就不一定