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简历中项目的功能进行提问,大家可以在评论区中解答/讨论;同时提供八股文