ThreadLocal
1.ThreadLocal是什么
ThreadLocal类让每一个线程都拥有了自己的本地变量,这意味着每个线程都可以独立地、安全地操作这些变量,而不会影响其他线程。
ThreadLocal的常用API
-
get():获取当前线程中与ThreadLocal对象关联的变量副本。
-
set(T value):将指定的值设置为当前线程中与ThreadLocal对象关联的变量副本。
-
remove():删除当前线程中与ThreadLocal对象关联的变量副本。这样可以避免内存泄漏问题。注意,remove()方法只会删除当前线程中的变量副本,不会影响其他线程中的副本。
-
initialValue():当调用get()或set()方法时,如果当前线程没有与ThreadLocal对象关联的变量副本,则会调用initialValue()方法创建一个新的变量副本并与当前线程关联。默认情况下,initialValue()方法返回null,可以通过继承ThreadLocal类并重写initialValue()方法来自定义初始化值。
2.ThreadLocal原理了解吗?
从 Thread 类源代码入手。
public class Thread implements Runnable {//......//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMap threadLocals = null;//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;//......
}
从上面 Thread 类中可以看出 Thread 类中有一个 threadLocals 和一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set 或 get 方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap 类对应的 get、set 方法。
ThreadLocal 类的 set 方法
public void set(T value) {//获取当前请求的线程Thread t = Thread.currentThread();//取出 Thread 类内部的 threadLocals 变量(哈希表结构)ThreadLocalMap map = getMap(t);if (map != null)// 将需要存储的值放入到这个哈希表中map.set(this, value);elsecreateMap(t, value);
}
ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread() 获取到当前线程对象后,直接通过 getMap(Thread t) 可以访问到该线程的ThreadLocalMap对象。
每个 Thread中 都具备一个 ThreadLocalMap,而 ThreadLocalMap 可以存储以 ThreadLocal 为 key ,Object 对象为 value 的键值对。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {//......
}
比如我们在同一个线程中声明了两个 ThreadLocal 对象的话, Thread内部都是使用仅有的那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。ThreadLocal 数据结构如下图所示。
ThreadLocalMap 是 ThreadLocal 的静态内部类。
3.ThreadLocal 内存泄露问题是怎么导致的?
内存泄漏和内存溢出的区别是什么
-
内存泄漏指的是程序中分配的内存在不再需要时没有被正确释放或回收的情况。
-
内存溢出指的是程序试图分配超过其可用内存的内存空间的情况。
ThreadLocal 对象和 ThreadLocalMap 中使用的 key 是弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉。但是,如果Thread 对象一直在被使用,比如在线程池中被重复使用,那么从Thread 对象到 value 的引用链就一直在,导致 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。
ThreadLocalMap 实现中已经考虑了这种情况,在调用 set、get、remove 方法的时候,会清理掉 key 为 null 的 Entry。因此,使用完 ThreadLocal 方法后最好手动调用一下 remove 方法,就可以在下一次 GC 的时候,把 key 为 null 的 Entry 清理掉。
线程池
1.什么是线程池?
线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
2.为什么要用线程池?
池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
-
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
3.如何创建线程池?
方式一:通过ThreadPoolExecutor构造函数来创建(推荐)
代码示例:
ExecutorService pools = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,new ArrayBlockingQueue<>(6),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
ExecutorService的常用方法
方式二:通过线程池的工具类 Executors 来创建。
4.为什么不推荐使用内置线程池?
因为通过 Executors 创建出来的内置线程池会让我们不够熟悉线程池的运行规则,会有资源耗尽的风险,而通过 ThreadPoolExecutor 构造函数来创建线程池能让我们更加明确线程池的运行规则,规避资源耗尽的风险。
4.线程池的参数
5.线程池的任务拒绝策略有哪些?
-
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出 RejectedExecutionException 异常。是默认的拒绝策略。
-
ThreadPoolExecutor.DiscardPolicy:丢弃新任务,但是不抛出异常。
-
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列中等待最久的任务,然后把当前任务加入队列中。
-
ThreadPoolExecutor.CallerRunsPolicy: 在调用线程池的execute方法的线程中运行被拒绝的任务,从而绕过线程池直接执行。
6.线程池常用的阻塞队列有哪些?
新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在阻塞队列中。
-
ArrayBlockingQueue:是一个由数组结构组成的有界阻塞队列。它按照先进先出的原则对元素进行排序。
-
LinkedBlockingQueue:是一个由链表结构组成的有界阻塞队列。它按照先进先出的原则对元素进行排序。因为其队列大小默认为 Integer.MAX_VALUE,所以实际上它可以看作是无界队列。
-
SynchronousQueue:是一个没有缓冲的阻塞队列,每个插入操作必须等待另一个线程的移除操作,反之亦然。因此,该队列没有任何内部容量,不能预先插入元素。
-
PriorityBlockingQueue:是一个支持优先级排序的无界阻塞队列。默认情况下元素采取自然顺序排列,也可以通过实现 Comparable 接口或者在构造时传入 Comparator 对象进行排序。
由 Excutors 创建出来的内置线程池选用了不同的阻塞队列,不同的阻塞队列具有不同的特性和适用场景,具体使用哪种队列需要根据实际需求来选择。例如,
-
如果需要控制队列大小且按照先进先出的顺序处理任务,可以选择 ArrayBlockingQueue 或 LinkedBlockingQueue;
-
如果需要无缓冲等待两个线程之间的交互,可以选择 SynchronousQueue;
-
如果需要按照优先级排序执行任务,可以选择 PriorityBlockingQueue。
7.线程池处理任务的流程了解吗?
-
如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
-
如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
-
如果任务队列已经满了导致任务投放任务失败,但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
-
如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,饱和策略会调用RejectedExecutionHandler.rejectedExecution() 方法
8.如何设定线程池的大小?
问题:很多人可能会觉得把线程池配置过大一点比较好。但是,线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。到底设置多少合适可以根据具体场景分析。
什么是上下文切换
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。Linux 相比与其他操作系统有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
CPU的8核16线程是什么意思
8核16线程指的是CPU能并行运行16线程,传统中,一个核心只能运行一个线程,但由英特尔公司开发的超线程技术硬件技术使得一个核心能并行运行多个线程。
9.如何设计一个能够根据任务的优先级来执行的线程池?
这是一个常见的面试问题,本质其实还是在考察求职者对于线程池以及阻塞队列的掌握。不同的线程池会选用不同的阻塞队列作为任务队列,比如 FixedThreadPool 使用的是LinkedBlockingQueue(无界队列),由于队列永远不会被放满,因此 FixedThreadPool 最多只能创建核心线程数的线程。ThreadPoolExecutor 的构造函数有一个 workQueue 参数可以传入任务队列。
设计方法:如果要实现一个优先级任务线程池的话,那可以考虑使用 PriorityBlockingQueue (优先级阻塞队列)作为任务队列。PriorityBlockingQueue 是一个支持优先级的无界阻塞队列,可以看作是线程安全的 PriorityQueue,两者底层都是使用小顶堆形式的二叉堆,即值最小的元素优先出队。不过,PriorityQueue 不支持阻塞操作。要想让 PriorityBlockingQueue 实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种:
-
提交到线程池的任务实现 Comparable 接口,并重写 compareTo 方法来指定任务之间的优先级比较规则。
-
创建 PriorityBlockingQueue 时传入一个 Comparator 对象来指定任务之间的排序规则(推荐)。
存在的问题
-
PriorityBlockingQueue 是无界的,可能堆积大量的请求,从而导致 OOM。
-
可能会导致饥饿问题,即低优先级的任务长时间得不到执行。
-
由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁 ReentrantLock),因此会降低性能。
解决方法
-
对于 OOM 这个问题的解决比较简单粗暴,就是继承PriorityBlockingQueue 并重写一下 offer 方法(入队)的逻辑,当插入的元素数量超过指定值就返回 false 。
-
饥饿问题这个可以通过优化设计来解决(比较麻烦),比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。
-
对于性能方面的影响,是没办法避免的,毕竟需要对任务进行排序操作。并且,对于大部分业务场景来说,这点性能影响是可以接受的。
PriorityQueue是queue系列中的一个集合