目录
传统的创建线程的方式(如继承 Thread 类、实现 Runnable 接口)会存在以下问题:
- 每次新建/销毁线程对象消耗资源、响应速度慢;
- 线程缺乏统一管理,容易出现阻塞情况。
JAVA 提供了线程池来解决以上问题,这也是推荐的使用方式。
创建线程池
线程池的创建方法:
new ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
);
ThreadPoolExecutor 各参数解析:
参数 | 含义 | 备注 |
corePoolSize | 线程池中的核心线程个数 | 默认情况下,核心线程一旦创建将一直存活,直到线程池关闭销毁。 |
maximummPoolSize | 线程池允许创建的最大线程个数 | 当线程池中的线程数达到该值后,新的任务加入到线程池中将会被阻塞。 |
keepAliveTime | 线程池中的非核心线程,处于闲置状态时的最长时间。 | 非核心线程闲置时间超过该值时,就会被系统回收销毁。 (如果设置 allowCoreThreadTimeOut 为 true 时,该值同样作用于核心线程)。 |
unit | keepAliveTime 的时间单位 | |
workQueue | 任务等待队列 | 该任务队列是一个阻塞队列(BlockQueue),当任务队列为空时,从中取任务会被阻塞;当任务队列满时,添加任务会被阻塞。 |
threadFactory | 创建线程的工厂 | 为线程池创建新线程,一些对线程的统一操作可以在这个类里实现。 |
handler | 当线程池无法处理任务时的处理方式 | 在 ThreadPoolExecutor 有 4 个实现 RejectedExecutionHandler 接口的静态内部类,代表 4 种不同的处理策略。 分别是:AbortPolicy(默认)、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy |
线程池的工作流程
- 新创建的线程池在没有调用 execute()/commit() 时内部没有任何线程(除非通过 prestartXXX() 预热)。
- 当有新任务时,首先判断当前核心线程数是否达到 corePoolSize,如果没有达到corePoolSize,则通过 threadFactory 创建一个核心线程。如果已经达到corePoolSize,则进入步骤3
- 判断 workQueue 是否满了,如果没满,则将任务加入到 workQueue 中,当核心线程中的任务处理完时,会自动从 workQueue 中取没有被执行的任务。如果 workQueue 也满了,则进入步骤4
- 判断当前线程是否达到 maximumPoolSize,如果没有达到 maximumPoolSize,则创建新的线程执行任务。如果已经达到 maximumPoolSize,则进入步骤 5
- 到这一步就是兜底策略了,ThreadPoolExecutor 中提供了四种 RejectedExecutionHandler:
- AbortPolicy:直接终止并抛出 RejectedExecutionException
- CallerRunsPolicy:直接在调用 ThreadPolExecutor.execute(Runnable) 的线程中执行当前 Runnable 的 run() 方法。(在 Android 中如果是在主线程中调用的,就会发生ANR)
- DiscardOldestPolicy:丢弃最早添加到队列中的任务,并尝试再次执行 execute() 方法。
- DiscardPolicy:默默的直接丢掉被拒绝的任务,不做其他任何操作,rejectedExecution() 方法是空实现。
对应流程图如下:
四种线程池
一般情况下,我们不需要自己创建线程池,Java 已经给我们提供了 4 种不同配置的线程池,以应对不同的使用场景。
线程池类型 | 特点 | 使用 场景 |
Fixed ThreadPool | 只有核心线程,数量由创建线程池时指定且固定不变;核心线程不会被系统自动关闭回收;当核心线程都运行时,添加新任务进入等待队列;任务队列无大小限制 | 控制最大并发线程数 |
Scheduled ThreadPool | 由核心线程 + 非核心线程组成。核心线程数由创建线程池时指定且固定不变;非核心线程数量无限制(闲置时马上被回收,源码中是 10ms,因为 0s 代表不回收);任务队列初始容量 16,支持动态扩容,无上限。 | 执行定时/周期性任务 |
Cached ThreadPool | 所有线程都是非核心线程。最大线程数量无限制;优先使用闲置线程处理新任务;无闲置线程时创建新线程;具备超时机制灵活回收空闲线程,线程空置时间60s,全部回收时几乎不占系统资源);任何线程任务到来都会立刻执行,不需要等待,因此等待队列中的任务数量永远为0。 | 执行大量、耗时少的线程任务。 |
SingleThread Executor | 有且仅有 1 个核心线程。当核心线程非空闲状态时有新任务添加进来,会先进入等待队列,等待队列容量没有上限,保证所有任务按照指定顺序在一个线程中执行,不需要处理线程同步的问题。 | 不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作,文件操作等。 |
关闭线程池
ExecutorService 提供了两个关闭线程池的方法:shutdown() 和 shutdownNow(),它们的区别如下:
- shutdown:设置线程池的状态为 SHUTDOWN,所有已经提交但没有执行完毕的任务会继续执行;再有新任务添加时会触发 RejectedExecutionHandler 执行。
- shutdownNow:设置线程池的状态为 STOP,然后尝试中断所有正在执行或暂停中的线程,并返回等待队列中的任务列表。
多线程的输入/输出流
多线程使用管道进行输入输出操作:
PipedOutputStream、PipedInputStream、PipedReader 和 PipedWriter 用于线程之间的数据传输,传输的媒介为内存。
多线程常见问题
多线程一定快吗?
多线程并不一定比串行快,因为线程有创建和上下文切换的开销。这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
如果一定要多线程,如何提高运行效率?
首先,多线程的开销是由于线程的创建和上下文切换导致的,所以需要从这两方面进行优化。
- 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建来很多线程来处理,这样会造成大量线程处于等待状态。
- 协程。在单线程里实现多任务多调度,并在单线程里维持多个任务间的切换。
- 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些方法来避免使用锁。
- CAS 算法。Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
如何避免死锁
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用 lock.tryLock(timeout) 来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在同一个数据库连接里,否则会出现解锁失败的情况。