|
@@ -102,3 +102,98 @@ AQS提供了一种通用的框架,用于实现线程间的协作和同步操
|
|
|
* LockSupport可以响应中断,而Object.wait()/notify()方法无法响应中断。
|
|
|
|
|
|
* LockSupport可以先执行unpark()方法,然后再执行park()方法,而Object.notify()方法必须在对应的wait()方法之前执行。
|
|
|
+
|
|
|
+## 线程池
|
|
|
+
|
|
|
+线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。线程池的优点和好处主要有以下三点
|
|
|
+
|
|
|
+* **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
|
|
|
+
|
|
|
+* **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。
|
|
|
+
|
|
|
+* **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
|
|
|
+
|
|
|
+### 常见的线程池:
|
|
|
+
|
|
|
+* **`FixedThreadPool`**:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
|
|
|
+
|
|
|
+* **`SingleThreadExecutor`:** 该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
|
|
|
+
|
|
|
+* **`CachedThreadPool`:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
|
|
|
+
|
|
|
+* **`ScheduledThreadPool`**:该返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池。
|
|
|
+
|
|
|
+### 为什么不推荐使用内置线程池?
|
|
|
+
|
|
|
+* **`FixedThreadPool` 和 `SingleThreadExecutor`**:使用的是无界的 `LinkedBlockingQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
|
|
|
+
|
|
|
+* **`CachedThreadPool`**:使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。
|
|
|
+
|
|
|
+* **`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor`** : 使用的无界的延迟阻塞队列`DelayedWorkQueue`,任务队列最大长度为 `Integer.MAX_VALUE`,可能堆积大量的请求,从而导致 OOM。
|
|
|
+
|
|
|
+总之一句话,他们的队列长度都是MAX_VALUE,可能导致OOM
|
|
|
+
|
|
|
+### 线程池核心参数
|
|
|
+
|
|
|
+* **`corePoolSize` :** 任务队列未达到队列容量时,最大可以同时运行的线程数量。
|
|
|
+
|
|
|
+* **`maximumPoolSize` :** 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
|
|
|
+
|
|
|
+* **`workQueue`:** 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
|
|
|
+
|
|
|
+1. 当线程数小于核心线程数时,创建线程。
|
|
|
+
|
|
|
+2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列
|
|
|
+
|
|
|
+3. 当线程数大于等于核心线程数,且任务队列已满
|
|
|
+
|
|
|
+ * 若线程数小于最大线程数,创建线程。
|
|
|
+
|
|
|
+ * 若线程数等于最大线程数,抛出异常,拒绝任务,执行拒绝策略
|
|
|
+
|
|
|
+### 线程池的饱和策略有哪些?
|
|
|
+
|
|
|
+* **`ThreadPoolExecutor.AbortPolicy`:** 抛出 `RejectedExecutionException`来拒绝新任务的处理。
|
|
|
+* **`ThreadPoolExecutor.CallerRunsPolicy`:** 调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
|
|
|
+* **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。
|
|
|
+* **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。
|
|
|
+
|
|
|
+我们这里使用自定义的饱和策略,即为新建一个线程去处理饱和任务
|
|
|
+
|
|
|
+### 如何设定线程池的大小?
|
|
|
+
|
|
|
+* **CPU 密集型任务(N+1)**
|
|
|
+
|
|
|
+* **I/O 密集型任务(2N)**
|
|
|
+
|
|
|
+CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
|
|
|
+
|
|
|
+## ThreadLocal
|
|
|
+
|
|
|
+通常情况下,我们创建的变量是可以被任何一个线程访问并修改的,想要每一个线程都有自己私有的本地变量就就需要使用ThreadLocal类,**`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
|
|
|
+
|
|
|
+`ThreadLocal`底层为每一个线程维护了一个`ThreadLocalMap`,他的set方法如下
|
|
|
+
|
|
|
+```java
|
|
|
+public void set(T value) {
|
|
|
+ //获取当前请求的线程
|
|
|
+ Thread t = Thread.currentThread();
|
|
|
+ //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
|
|
|
+ ThreadLocalMap map = getMap(t);
|
|
|
+ if (map != null)
|
|
|
+ // 将需要存储的值放入到这个哈希表中
|
|
|
+ map.set(this, value);
|
|
|
+ else
|
|
|
+ createMap(t, value);
|
|
|
+}
|
|
|
+ThreadLocalMap getMap(Thread t) {
|
|
|
+ return t.threadLocals;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## ThreadLocal 内存泄露问题是怎么导致的?
|
|
|
+
|
|
|
+`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
|
|
|
+
|
|
|
+这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。`ThreadLocalMap` 实现中已经考虑了这种情况,使用完 `ThreadLocal`方法后最好手动调用`remove()`方法
|
|
|
+
|