多线程与线程池

传统的创建线程的方式(如继承 Thread 类、实现 Runnable 接口)会存在以下问题:

  1. 每次新建/销毁线程对象消耗资源、响应速度慢;
  2. 线程缺乏统一管理,容易出现阻塞情况。

JAVA 提供了线程池来解决以上问题,这也是推荐的使用方式。

创建线程池

线程池的创建方法:

new ThreadPoolExecutor(
   int corePoolSize,
   int maximumPoolSize,
   long keepAliveTime,
   TimeUnit unit,
   BlockingQueue<Runnable> workQueue,
   ThreadFactory threadFactory,
   RejectedExecutionHandler handler
);

ThreadPoolExecutor 各参数解析:

参数含义备注
corePoolSize线程池中的核心线程个数默认情况下,核心线程一旦创建将一直存活,直到线程池关闭销毁。
maximummPoolSize线程池允许创建的最大线程个数当线程池中的线程数达到该值后,新的任务加入到线程池中将会被阻塞。
keepAliveTime线程池中的非核心线程,处于闲置状态时的最长时间。非核心线程闲置时间超过该值时,就会被系统回收销毁。
(如果设置 allowCoreThreadTimeOut 为 true 时,该值同样作用于核心线程)。
unitkeepAliveTime 的时间单位
workQueue任务等待队列该任务队列是一个阻塞队列(BlockQueue),当任务队列为空时,从中取任务会被阻塞;当任务队列满时,添加任务会被阻塞。
threadFactory创建线程的工厂为线程池创建新线程,一些对线程的统一操作可以在这个类里实现。
handler当线程池无法处理任务时的处理方式在 ThreadPoolExecutor 有 4 个实现 RejectedExecutionHandler 接口的静态内部类,代表 4 种不同的处理策略。
分别是:AbortPolicy(默认)、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy

线程池的工作流程

  1. 新创建的线程池在没有调用 execute()/commit() 时内部没有任何线程(除非通过 prestartXXX() 预热)。
  2. 当有新任务时,首先判断当前核心线程数是否达到 corePoolSize,如果没有达到corePoolSize,则通过 threadFactory 创建一个核心线程。如果已经达到corePoolSize,则进入步骤3
  3. 判断 workQueue 是否满了,如果没满,则将任务加入到 workQueue 中,当核心线程中的任务处理完时,会自动从 workQueue 中取没有被执行的任务。如果 workQueue 也满了,则进入步骤4
  4. 判断当前线程是否达到 maximumPoolSize,如果没有达到 maximumPoolSize,则创建新的线程执行任务。如果已经达到 maximumPoolSize,则进入步骤 5
  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) 来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在同一个数据库连接里,否则会出现解锁失败的情况。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注