JUC面试题

ThreadLocal的理解

场景1:每个线程需要一个独享的对象【通常是工具类,典型需要使用的类有SimpleDateFormat和Random】【每个Thread内有自己的实例副本,不共享;比如教材只有一本,一起做笔记会有线程安全问题,复印后就没问题了】

场景2:每个线程内需要保存全局变量【例如在拦截器中获取用户信息】可以让不同方法直接使用,避免参数传递的麻烦

作用:让某个需要用到的对象在线程间隔离【每个线程都有自己的独立对象】

优点:保证线程安全;不需要加锁,提高执行效率;更高效的利用内存,节省空间;免去传参的繁琐,降低代码耦合度

Get方法过程:得到这个线程对应的value。如果是首次调用get,则会执行initailValue来得到这个值

先获取当前线程的ThreadLocalMap,然后调用map.getEntry方法,把当前ThreadLocal的引用作为参数传入,取出map中属于当前ThreadLocal的value

ThreadLocalMap以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中

set方法过程:为这个线程设置一个新值

首先获取当前线程的threadLocalMap,判断是否为Null,如果不为null则传入key-value,如果为null,则创建一个新的threadLocalMap,并传入key-value

remove方法过程:

首先通过当前线程获取threadLocalMap,然后判断是否为Null,不为Null,则删除当前线程的value

InitialValue方法过程:

该方法会返回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get方法的时候,才会触发

当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用initialValue方法

通常每个线程最多调用一次此方法,但如果已经调用了remove方法,再调用get方法,则可以再次调用此方法

如果不重写initailValue方法,这个方法会返回null。一般使用匿名内部类的方法来重写initailValue方法,以便在后续使用中可以初始化副本对象

Thread,ThreadLocal以及ThreadLocalMap三者之间的关系?

每个Thread对象都持有一个ThreadLocalMap成员变量;ThreadLocalMap 是 ThreadLocal 的内部类,用来保存ThreadLocal,一个线程可能有多个ThreadLocal

ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:

键:当前ThreadLocal 值:实际需要的成员变量

ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链

内存泄露问题:

A.ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry都包含了一个对value的强引用

B.正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了

C.但是,如果线程不终止【比如线程需要保持很久】那么key对应的value就不能被回收,因为有以下的调用链

【Thread-ThreadLocalMap-Entry(key为Null)-value】

D.因为value和thread之间还存在强引用链路,所以导致value无法回收,就会出现内存泄露

E.JDK也考虑到这个问题,所以在set,remove,rehash方法中会扫描key为Null的Entry,并把对应的value设置为Null,这样value对象就可以被回收

F.但是如果一个ThreadLocal不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏

如何避免内存泄露

调用remove方法,就会删除对应的entry对象,可以避免内存泄露,所以使用完ThreadLocal之后,应该调用remove方法

空指针异常【在进行get之前,必须先set,否则可能会报空指针异常】

装箱拆箱可能会导致空指针异常

共享对象:

如果在每个线程中ThreadLocal.set进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get取得的还是这个共享对象本身,还是会有并发访问的问题

如果可以不使用ThreadLocal就解决问题,那么不要强行使用

例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLoal

优先使用框架的支持,而不是自己创造

例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove方法,造成内存泄露

Spring中的使用

DateTimeContextHolder,RequestAttributesHolder等

每个HTTP请求都对应一个线程,线程之间相互隔离

线程池

线程池构造函数的参数

corePoolSize【int】核心线程数

maxPoolSize【int】最大线程数

keepAliveTime【long】保持存活时间【如果线程池中当前线程数多于核心线程数,如果多余的线程空闲时间超过keepAliveTime,他们就会被终止】

workQueue【BlockingQueue】任务存储队列

常见的队列类型:

直接交接:SynchronousQueue【获取到任务,马上移交给执行者】

无界队列:LinkedBlockingQueue

有界队列:ArrayBlockingQueue

threadFactory【ThreadFactory】当线程池需要新的线程的时候,会使用threadFactory来生成新的线程【新的线程都由ThreadFactory创建,默认使用Executors.defaultThreadFactory(),创建出来的线程都在同一个线程组,拥有同样的NORM_PRIORITY优先级并且都不是守护线程。如果自己指定ThreadFactory,那么就可以改变线程名,线程组,优先级,是否是守护线程】

Handler【RejectedExecutionHandler】由于线程池无法接受你所提交的任务的拒绝策略

添加线程规则

如果线程数小于核心线程数【corePoolSize】,即使其他工作线程处于空闲状态,也会创建一个新线程来运行新任务

如果线程数等于或者大于核心线程数【corePoolSize】但少于最大线程数【maxPoolSize】,则将任务放入队列中

如果队列满了,并且线程数小于最大线程数【maxPoolSize】则创建一个新线程来运行任务

如果队列满了,并且线程数大于或者等于最大线程数【maxPoolSize】,则拒绝该任务

增减线程的特点

通过设置核心线程数【corePoolSize】和最大线程数【maxPoolSize】相同,就可以创建固定大小的线程池

线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它

通过设置最大线程数【maxPoolSize】为很高的值,例如Integer.MAX_VALUE,可以允许线程池容纳任意数量的并发任务

只有在队列填满时才创建多于核心线程数【corePoolSize】的线程,所以如果你使用的是无界队列,那么线程数就不会超过核心线程数【corePoolSize】

新任务提交executor执行后的判断

如果运行的线程少于corepoolsize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的

如果线程池中的线程数量大于等于corepoolsize且小于maximumpoolsize,则只有当workqueue满时才创建新的线程去处理任务

如果设置的corepoolsize和maximumpoolsize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workqueue未满,则将请求放入workqueue中,等待有空闲的线程去从workqueue中取任务并处理

如果运行的线程数量大于等于maximumpoolsize,这时如果workqueue已经满了,则通过handler所指定的策略来处理任务

线程池应该手动创建还是自动创建?

手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险;需要根据不同的业务场景,自己设置线程池的参数

自动创建的风险

new FixedThreadPool:

由于传进去的LinkedBlockingQueue是没有容量上限的,所以当请求数越来越多,并且无法及时处理完毕的时候,也就是请求堆积的时候,会容易造成占用大量的内存,可能会导致内存溢出

new SingleThreadExecutor:

使用LinkedBlockingQueue无边界队列,线程数直接设置为1,所以会导致当请求堆积的时候,可能会占用大量的内存

new cachedThreadPool:【可缓存线程池,无界线程池,具有自动回收多余线程的功能】由于最大线程数被设置为Integer.MAX_VALUE,可能会创建数量非常多的线程,导致内存泄露,使用直接交接队列:SynchronousQueue

new ScheduledThreadPool:【支持定时及周期性任务执行的线程池】

使用无界延迟队列,核心线程数设置过小,会导致任务延迟处理,可能会导致内存溢出。核心线程数设置过大,如果并发任务少,会造成大量的线程浪费

停止线程池的正确方法

Shutdown:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池

isShutdown:判断是否进入停止状态

isTerminated:判断线程池停止后,线程是否执行完成

awaitTermination:测试线程是否会在一定时间内停止

shutdownNow:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行,返回没有执行完成的线程

拒绝策略:

拒绝时间:【当线程池关闭时,提交新任务会被拒绝;当线程池已经使用最大线程数,并且工作队列容量使用有限边界已经饱和时,拒绝提交新的任务】

AbortPolicy:抛出异常,没有提交成功

DiscardPolicy:丢弃任务,不会提示

DiscardOldestPolicy:丢弃时间比较久没有处理的任务,以便提交新的任务

CallerRunsPolicy:当线程池无法再继续处理新提交的任务,那么就交给提交者去执行;【优点:避免业务损失,降低提交速度,提交者执行期间,线程池也会执行完一些任务,可以再次接受新任务】

线程池组成部分

线程池管理器,工作线程,任务队列,任务接口

线程池状态:

Running:能够接受新提交的任务,并且也能处理阻塞队列中的任务

Shutdown:不再接受新提交的任务,但可以处理存量任务

Stop:不再接受新提交的任务,也不处理存量任务

Tidying:所有的任务都已经终止

Terminated:terminated方法执行完成后进入该状态

使用线程池的注意点

避免任务堆积;避免线程数过度增加;排查线程泄露

原子类

原子操作:一个操作不可中断,即便是多线程的情况下也可以保证

原子类的作用:保证并发情况下线程安全

优点:粒度更细,原子变量可以把竞争范围缩小到变量级别

Atomic基本类型原子类【基于CAS】

Atomic*Array【数组类型原子类】

Atomic*Reference引用类型原子类

Atomic*Field

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

Java之项目解析+八股文 文章被收录于专栏

针对Java简历中项目的功能进行提问,大家可以在评论区中解答/讨论;同时提供八股文

全部评论

相关推荐

5 17 评论
分享
牛客网
牛客企业服务