多线程

43 阅读5分钟

多线程

引言

在Android开发中,网络请求、图片加载等耗时操作不可避免,为解决其阻塞UI线程问题,通常会选择 线程协程 来处理。本文主要介绍线程使用

线程

  • 继承Thread
class T: Thread() {
    override fun run() {
        // ...
    }
}

fun task() {
    val thread = T()
    thread.start()
}
  • 实现Runnable接口
class R: Runnable {
    override fun run() {
        // ...
    }
}

fun task() {
    val thread = Thread(R())
    thread.start()
}
  • FutureTask创建任务实现call方法
fun task() {
    val task: FutureTask<Int> = FutureTask(object: Callable<Int> {
        override fun call(): Int {
            // ...
            LogTool.i("threadName: ${Thread.currentThread().name}")
            return 0
        }
    })
    val thread = Thread(task)
    thread.start()
    LogTool.i("task call(): ${task.get()}")
}

线程池

通过维护一组可重用的线程,降低线程创建和销毁的开销。线程池可使线程使用更加高效,便于管理控制线程并发数量,杜绝系统资源浪费

生命周期

  • 创建 -> 运行 -> 关闭
  • 可通过 shutdown()shutdownNow() 关闭

优势

  • 线程复用。降低创建和销毁开销,提高系统性能
  • 资源控制。限制线程数量,规避资源浪费和竞争
  • 队列管理。确保任务按序执行,提高执行效率

内置线程池(Executors)

java.util.concurrent.Executors 提供一系列创建线程池方法,简化了线程池的创建

无界线程池newCachedThreadPool

  • 可动态调整线程数量。空闲线程(线程数 - 任务数)存活时间为60s
  • 任务处理速度快。适用于处理大量短时且不确定任务量的场景
  • 会导致资源耗尽。由于线程数量无限制,大量任务提交时,会创建大量线程,导致系统资源耗尽(内存)
  • 会过度竞争。高并发情况下,大量线程创建会导致线程之间竞争,从而影响性能
  • 会引起任务堆积。若任务执行时间长且新任务不断提交,会导致线程池中堆积大量未完成任务,影响系统稳定性
  • 不适用于长期运行任务。对于长期运行任务,无界线程池会导致创建大量的线程,而这些线程在任务完成后不会被回收,最终会耗尽系统资源

长时间运行的任务或负载较高场景可使用有界线程池或使用任务队列进行任务排队

示例

// val cachedService: ExecutorService = Executors.newCachedThreadPool()
val cachedService = Executors.newCachedThreadPool(object : ThreadFactory {
    override fun newThread(r: Runnable?): Thread {
        val thread = Thread(r)
        thread.name = "Custom-${thread.id}"
        thread.priority = Thread.NORM_PRIORITY
        thread.isDaemon = false
        return thread
    }
})

cachedService.submit(() -> {
    
})

// 关闭
cachedService.shutdown()

有界线程池newFixedThreadPool(size: Int)

  • 固定线程池中线程数量,超出则在队列中等待。
  • 适用于并发数有限场景,可控制最大并发数
  • 灵活性差。线程数固定,负载大时,线程不足(例瞬时高并发时无法及时响应导致一些任务需等待执行);负载较小时,浪费资源
  • 可能引起线程饥饿。若设定线程数小且任务提交多或速度快,会导致部分任务一直等待执行,产生线程饥饿情况

示例

// 有界线程池
val fixedService1 = Executors.newFixedThreadPool(3)
val fixedService2 = Executors.newFixedThreadPool(3) { r ->
    val thread = Thread(r)
    thread.name = "Custom-${thread.id}"
    thread.priority = Thread.NORM_PRIORITY
    thread.isDaemon = false
    thread
}

/**
 * 有界线程池执行、关闭
 */
fun fixedThreadPool(runnable: Runnable) {
    fixedService1.submit(runnable)
    fixedService1.shutdown()
}

单线程线程池newSingleThreadExecutor

  • 只有一个核心线程的线程池,保证所提交任务按序执行
  • 线程复用。减少线程创建和销毁开销,提高了线程复用性
  • 异常管理。若任务异常导致线程终止,单线程池会创建一个新线程取代原线程,确保线程池中总有一个可用的线程,防止因异常而导致整个应用crash
  • 便于任务队列的管理。池内维护一个任务队列,将任务按提交顺序排队,有助于管理任务的执行顺序和控制任务的并发度
  • 适用于需按序执行任务的场景(例任务相依赖),以无界队列存储任务

虽有些场景一个线程足矣,但单线程池的引入可更好的控制和管理任务,使其更易于维护和调试。同时,线程池的使用也符合并发编程的最佳实践,能有效管理线程生命周期,防止资源泄露和浪费。所以单线程池使用优于单线程使用

调度线程池newScheduledThreadPool(corePoolSize: Int)

  • 在newFixedThreadPool基础上增加了 定时执行 功能
  • 适用于定期执行任务场景。例定时任务、周期性数据同步

自定义线程池(ThreadPoolExecutor)

可通过 java.util.concurrent.ThreadPoolExecutor 自定义线程池

  • corePoolSize:核心线程数,线程池中最少的线程数
  • maxPoolSize:最大线程数,线程池中最多的线程数
  • keepAliveTime:空闲线程(池中线程数 > 核心线程数时)在终止前等待新任务时间
  • unit:keepAliveTime单位
  • workQueue:保存等待执行任务的阻塞队列
  • factory:创建线程的工厂(可不指定),默认使用ThreadPoolExecutor.defaultThreadFactory创建线程
  • handler:拒绝策略,当 workQueue 已满且池中线程数等于 maxPoolSize 时,线程池拒绝新任务添加时的策略(可不指定),默认策略为抛异常RejectedExecutionException
ThreadPoolExecutor(
    corePoolSize: Int,
    maxPoolSize: Int,
    keepAliveTime: Long,
    unit: TimeUnit,
    workQueue: BlockingQueue<Runnable>,
    factory: ThreadFactory,
    handler: RejectedExecutionHandler)

阻塞队列

存储等待执行任务的队列

LinkedBlockingQueue

  • 继承自AbstractQueue,实现BlockingQueue
  • 基于链表的阻塞队列,容量可不设置(默认Int.MAX_VALUE)
  • 具有较高性能和吞吐量(单位时间可处理的工作量或数据量)
  • 若任务提交速度 > 线程处理速度,队列无限增大,占用大量内存
  • Executors.newSingleThreadExecutor()使用该队列
  • Executors.newFixedThreadPool()使用该队列

ArrayBlockingQueue

  • 继承自AbstractQueue,实现BlockingQueue
  • 基于数组的有界阻塞队列,容量必须设置
  • 可限制队列容量,避免无限增长
  • 需合理设置容量,否则大了浪费资源,小了导致性能瓶颈

SynchronousQueue

  • 继承自AbstractQueue,实现BlockingQueue,可替换为ArrayBlockingQueue,但比其更轻量级
  • 无容量的阻塞队列(链表),轻量级,插入操作必须在另一个线程移除后
  • 目的是保证对提交任务,有空闲线程复用处理,无则新建线程处理
  • Executors.newCachedThreadPool()使用该队列

PriorityBlockingQueue

  • 有优先级的无界阻塞队列(数组),可设置优先级,实现按序执行任务
  • 高并发场景下,存在线程竞争,性能会受影响
  • 任务类需实现 Comparable 或 构造传入自定义比较器PriorityBlockingQueue(capacity: Int, comparator: Comparator< in E?>?)
示例
/**
 * 任务
 */
class Task(val priority: Int = 0, val info: String = "normal"): Comparable<Task> {
    /**
     * 降序
     */
    override fun compareTo(other: Task): Int = other.priority.compareTo(this.priority)
}

/**
 * 使用
 */
fun test() {
    val queue: PriorityBlockingQueue<Task> = PriorityBlockingQueue()
    queue.put(Task(5, "Medium"))
    queue.put(Task(3, "Low"))
    queue.put(Task(7, "High"))
    while(!queue.isEmpty()) {
        val task = queue.task()
        LogTool.i("task: $task")
    }
}

拒绝策略

interface RejectedExecutionHandler

已有实现类AbortPolicyCallerRunsPolicyDiscardOldestPolicyDiscardPolicy

可自定义策略实现RejectedExecutionHandler接口

  • AbortPolicy:默认策略,无法接受新任务时抛异常RejectedExecutionException
  • CallerRunsPolicy:该策略会直接在提交任务的线程中执行被拒的任务,适用于负载小、执行时间短的任务
  • DiscardOldestPolicy:该策略会直接丢弃任务
  • DiscardPolicy:该策略会丢弃最早提交但未被执行的任务,新任务入队

自定义线程池示例

val threadPool =
    ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, ArrayBlockingQueue(10), object: ThreadFactory {
        val threadNum = AtomicInteger(1)

        override fun newThread(r: Runnable?) =
            Thread(r, "XThread-${threadNum.getAndIncrement()}")
    }) { r, executor -> LogTool.e("$r rejected, completedTaskCount: ${executor.completedTaskCount}") }
    
// 执行,无结果
threadPool.execute { }

// 执行,有结果
val future = mThreadPool.submit { }
future.get()

线程池使用优化

  • 资源释放,关闭线程池 shutdown()shutdownNow()
  1. 任务完成后,及时关闭。
  2. 任务取消,线程结束。
  • 弱引用外部对象,避免内存泄露
  • 监控线程池状态,规避问题
  1. 定时记录线程池状态信息。在执行的任务数、已完成任务数、任务总数(已执行+未执行)、队列信息
  • 使用Android Studio自带Android Profiler检测工具,查看线程池信息,从而优化使用