读书笔记之Java并发编程的艺术-四

44次阅读

共计 3590 个字符,预计需要花费 9 分钟才能阅读完成。

由于内容过多,分一个系列来写,这是第四篇。

九、Java 中的线程池

线程池用于异步或并发执行任务的场景,合理使用线程池带来的好处:
(1)降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗;
(2)提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行;
(3)提高线程的可管理性,使用线程池统一分配、调优和监控。

1、线程池的实现原理

当向线程池提交一个任务后,线程池的主要处理流程:
(1)线程池判断核心线程池里的线程是否都在执行任务,如果不是,那么创建一个新的工作线程来执行任务,如果核心线程池里的线程都在执行任务,那么进入下一步;
(2)线程池判断工作队列是否满了,如果工作队列没满,则将新提交的任务存储在这个工作队列中,如果满了,进行下一步;
(3)线程池判断线程池的线程是否都处于工作状态,如果不是,则创建一个新的工作线程来执行任务,如果满了,则交给饱和策略来处理这个任务。

ThreadPoolExecutor 执行 execute 方法分为下面四种情况:
(1)如果当前运行的线程少于 corePoolSize,那么创建新线程来执行任务 (执行这一步需要获得全局锁);
(2) 如果运行的线程等于或多于 corePoolSize,则将任务加入 BlockingQueue;
(3)如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处理任务 (执行这一步需要获取全局锁);
(4) 如果创建新线程将使当前运行的线程超过 maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。

注意:ThreadPoolExecutor 采取上述步骤的总体设计思路,是为了在执行 execute()方法时,尽可能的避免获取全局锁,ThreadPoolExecutor 在完成预热后 (当前运行的线程数大于等于 corePoolSize),几乎所有的 execute() 方法调用都在执行上面的步骤 2,步骤 2 是不需要获得全局锁的。

工作线程:线程池创建线程时,会把线程封装成工作线程 Worker,Worker 在执行完任务后,还会循环获取工作队列里的任务来执行。

2、线程池的创建

创建线程池的几个参数:
(1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于 corePoolSize 时就不在创建。调用 prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程;
(2)workQueue(任务队列):用于保存等待执行的任务的阻塞队列,比如 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、
PriorityBlockingQueue;
(3)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数,如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。注意:如果使用了无界的任务队列,这个参数就没效果了;
(4)threadFactory:用于创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字;
(5)handler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采用一种策略处理提交的新任务,默认的策略是 AbortPolicy——直接抛出异常,还有其他三种策略,分别是 CallerRunsPolicy——只用调用者所在线程来运行任务,DiscardPolicy——不处理,丢弃掉,DiscardOldestPolicy——丢弃队列里最前面
的任务,并执行当前任务;
(6)keepAliveTime(多余空闲线程的最长存活时间):当线程池中线程数量大于 corePoolSize,会根据 keepAliveTime 的值进行活性检查,一旦超时便销毁
大于 corePoolSize 小于等于 maximumPoolSize 的线程;
(7)unit:keepAliveTime 的时间单位。

3、向线程池提交任务

可以使用两个方法向线程池提交任务,分别是 execute()和 submit()方法。execute()用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功。submit()用于提交需要返回值的任务,线程池会返回一个 future 类型的对象,通过 future 对象可以判断任务是否执行成功。

4、关闭线程池

可以通过调用线程池里的 shutdown()或 shutdownNow()方法来关闭线程池。

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor} finally {mainLock.unlock();
    }
    tryTerminate();}

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();} finally {mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

5、合理分配线程池

根据不同任务特性,合理分配线程池,可以从这几个角度分析:任务的类型 (CPU 密集型、IO 密集型、混合型任务)、任务的优先级(高、中、低)、
任务的执行时间(长、中、短)、任务的依赖性(是否依赖其他系统资源,比如数据库连接)。

6、线程池的监控

对线程池进行监控,方便出问题时候,可以根据线程池的使用状况快速定位问题。

十、Executor 框架

1、Executor 框架简介

java 多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器 (Executor 框架) 将这些任务映射为固定数量的线程;在底层,操作系统内
核将这些线程映射到硬件处理器上。也就是应用程序通过 Executor 框架控制上层的调度,而下层的调度由操作系统内核控制,下层的调度不受应用
程序的控制。

Executor 框架主要由三大部分组成:
(1)任务 -> Runnable 接口、Callable 接口;
(2)任务的执行 -> Executor 接口、ExecutorService 接口、ThreadPoolExecutor 实现类、ScheduledThreadPoolExecutor 实现类;
(3)异步计算的结果 -> Future 接口、FutureTask 实现类。

ThreadPoolExecutor 通常使用工厂类 Executors 来创建,Executors 可以创建几种类型的 ThreadPoolExecutor;
(1)FixedThreadPool:适应于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,适用于负载比较重的服务器;
(2)SingleThreadExecutor:适用于需要保证顺序地执行各个任务,并且在任意时间点,不会有多个线程是活动的应用场景;
(3)CachedThreadPool:是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者负载较轻的服务器。

ScheduledThreadPoolExecutor 通常使用工厂类 Executors 来创建,适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要
限制后台线程的数量的应用场景。

FixedThreadPool 和 SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列,CachedThreadPool 使用没有容量的
SynchronousQueue 作为线程池的工作队列,但 CachedThreadPool 的 maximumPoolSize 是无界的,这意味着如果主线程提交任务的速度高于
maximumPoolSize 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程,会导致耗尽 CPU 和内存资源。SynchronousQueue 是一个
没有容量的阻塞队列,每个插入操作必须等待另一个线程的对应移除操作。

正文完
 0