Java - 线程池

本文介绍Java中Executors提供的四种线程池,包括newCachedThreadPool、newFixedThreadPool等,阐述线程池的参数、执行流程,如任务来临时根据线程数、队列情况决定处理方式。还提及线程池监控方法,可利用参数或扩展线程池监控,以及判断任务是否执行完的几种方式。

Executors提供四种线程池

Executors类提供4个静态工厂方法:

工厂方法corePoolSizemaximumPoolSizekeepAliveTimeworkQueue
newCachedThreadPool0Integer.MAX_VALUE60sSynchronousQueue
newFixedThreadPoolnThreadsnThreads0LinkedBlockingQueue
newSingleThreadExecutor110LinkedBlockingQueue
newScheduledThreadPoolcorePoolSizeInteger.MAX_VALUE0DelayedWorkQueue

其他参数都相同,其中线程工厂的默认类为DefaultThreadFactory,线程饱和的默认策略为ThreadPoolExecutor.AbortPolicy。 

这些方法最终都是通过ThreadPoolExecutor类来完成的

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

corePoolSize(线程池基本大小)必须大于或等于0;
maximumPoolSize(线程池最大大小)必须大于或等于1;
maximumPoolSize必须大于或等于corePoolSize;
keepAliveTime(线程存活保持时间)必须大于或等于0;
workQueue(任务队列)不能为空;
threadFactory(线程工厂)不能为空,默认为DefaultThreadFactory类
handler(线程饱和策略)不能为空,默认策略为ThreadPoolExecutor.AbortPolicy。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
            null :
            AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

参数说明:

corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,才会根据是否存在空闲线程,来决定是否需要创建新的线程。除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。


maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。


keepAliveTime(线程存活保持时间):默认情况下,当线程池的线程个数多于corePoolSize时,线程的空闲时间超过keepAliveTime则会终止。但只要keepAliveTime大于0,allowCoreThreadTimeOut(boolean) 方法也可将此超时策略应用于核心线程。另外,也可以使用setKeepAliveTime()动态地更改参数。


unit(存活时间的单位):时间单位,分为7类,从细到粗顺序:NANOSECONDS(纳秒),MICROSECONDS(微妙),MILLISECONDS(毫秒),SECONDS(秒),MINUTES(分),HOURS(小时),DAYS(天);


workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。可以使用此队列与线程池进行交互:
如果运行的线程数少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
如果运行的线程数等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。


threadFactory(线程工厂):用于创建新线程。由同一个threadFactory创建的线程,属于同一个ThreadGroup,创建的线程优先级都为Thread.NORM_PRIORITY,以及是非守护进程状态。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号);

handler(线程饱和策略):当线程池和队列都满了,则表明该线程池已达饱和状态。
ThreadPoolExecutor.AbortPolicy:处理程序遭到拒绝,则直接抛出运行时异常 RejectedExecutionException。(默认策略)
ThreadPoolExecutor.CallerRunsPolicy:调用者所在线程来运行该任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
ThreadPoolExecutor.DiscardPolicy:无法执行的任务将被删除。
ThreadPoolExecutor.DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重新尝试执行任务(如果再次失败,则重复此过程)。

队列排队详解
直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。


无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。


有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

实现BlockingQueue接口的常见类如下:

ArrayBlockingQueue:基于数组的有界阻塞队列。队列按FIFO原则对元素进行排序,队列头部是在队列中存活时间最长的元素,队尾则是存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。 这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。ArrayBlockingQueue构造方法可通过设置fairness参数来选择是否采用公平策略,公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”,可根据情况来决策。
LinkedBlockingQueue:基于链表的无界阻塞队列。与ArrayBlockingQueue一样采用FIFO原则对元素进行排序。基于链表的队列吞吐量通常要高于基于数组的队列。
SynchronousQueue:同步的阻塞队列。其中每个插入操作必须等待另一个线程的对应移除操作,等待过程一直处于阻塞状态,同理,每一个移除操作必须等到另一个线程的对应插入操作。SynchronousQueue没有任何容量。不能在同步队列上进行 peek,因为仅在试图要移除元素时,该元素才存在;除非另一个线程试图移除某个元素,否则也不能(使用任何方法)插入元素;也不能迭代队列,因为其中没有元素可用于迭代。Executors.newCachedThreadPool使用了该队列。
PriorityBlockingQueue:基于优先级的无界阻塞队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。优先级队列不允许使用 null 元素。依靠自然顺序的优先级队列还不允许插入不可比较的对象(这样做可能导致 ClassCastException)。虽然此队列逻辑上是无界的,但是资源被耗尽时试图执行 add 操作也将失败(导致 OutOfMemoryError)。

 线程工厂

通过线程工厂可以设置线程池中可以创建的最大线程数,设置线程的名称,线程的优先级以及线程的类型等内容。

public static class testThreadPoolFactory implements ThreadFactory {

    private AtomicInteger threadIdx = new AtomicInteger(0);

    private String threadNamePrefix;

    public testThreadPoolFactory(String Prefix) {
        threadNamePrefix = Prefix;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r);
        thread.setName(threadNamePrefix + "-xxljob-" + threadIdx.getAndIncrement());
        return thread;
    }
}
ThreadFactory threadFactory = new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        // 创建线程池中的线程
        Thread thread = new Thread(r);
        // 设置线程名称
        thread.setName("Thread-" + r.hashCode());
        // 设置线程的优先级 (1-10) 最大 10
        thread.setPriority(Thread.MAX_PRIORITY);
        // 设置线程的类型 (前台/后台线程)
        thread.setDaemon(false);
        // ....
        return thread;
    }
};

 线程池的拒绝策略

拒绝策略一共有 5 种,线程池内置了 4 种和一种自定义拒绝策略,这四种拒绝策略分别是 :

AbortPolicy:终止策略,线程池会抛出异常并终止执行此任务

CallerRunsPolciy:把任务交给添加此任务的(main)线程来执行

DiscardPolicy:忽略当前任务(最新的任务)

DiscardOldestPolicy:忽略任务队列中最早加入的任务,接收新任务(加入任务队列)

线程池默认的拒绝策略为 AbortPolicy 终止策略。

 自定义拒绝策略

// 创建 runnable 对象
// ...
 
ThreadPoolExecutor threadPool =
   new ThreadPoolExecutor(1, 1,
   100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
   new RejectedExecutionHandler() {
       @Override
       public void rejectedExecution(Runnable r,ThreadPoolExecutor executor) {
           // 执行自定义拒绝策略
           // 1.记录日志,方便问题的追溯
           // 2.通知相关人员来解决问题
           System.out.println("执行自定义拒绝策略");
       }
   });
// 添加并执行 3 个任务
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);


newCachedThreadPool:

创建一个可缓存的无界线程池,如果线程池长度超过处理需要,可灵活回收空线程,若无可回收,则新建线程。当线程池中的线程空闲时间超过60s,则会自动回收该线程,当任务超过线程池的线程数则创建新的线程,线程池的大小上限为Integer.MAX_VALUE,可看作无限大。

/**
 * 可缓存无界线程池测试
 * 当线程池中的线程空闲时间超过60s则会自动回收该线程,核心线程数为0
 * 当任务超过线程池的线程数则创建新线程。线程池的大小上限为Integer.MAX_VALUE,
 * 可看做是无限大。
 */
@Test
public void cacheThreadPoolTest() {
    // 创建可缓存的无界线程池,可以指定线程工厂,也可以不指定线程工厂
    ExecutorService executorService = Executors.newCachedThreadPool(new testThreadPoolFactory("cachedThread"));
    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
            print("cachedThreadPool");
            System.out.println(Thread.currentThread().getName());
                }
        );
    }
}


newFixedThreadPool:

创建一个指定大小的线程池,可控制线程的最大并发数,超出的线程会在LinkedBlockingQueue阻塞队列中等待

/**
 * 创建固定线程数量的线程池测试
 * 创建一个固定大小的线程池,该方法可指定线程池的固定大小,对于超出的线程会在LinkedBlockingQueue队列中等待
 * 核心线程数可以指定,线程空闲时间为0
 */
@Test
public void fixedThreadPoolTest() {
    ExecutorService executorService = Executors.newFixedThreadPool(5, new testThreadPoolFactory("fixedThreadPool"));
    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
                    print("fixedThreadPool");
                    System.out.println(Thread.currentThread().getName());
                }
        );
    }
}


newScheduledThreadPool:

创建一个定长的线程池,可以指定线程池核心线程数,支持定时及周期性任务的执行

/**
 * schedule(Runnable command, long delay, TimeUnit unit)
 * 延迟一定时间后执行Runnable任务;
 * <p>
 * schedule(Callable callable, long delay, TimeUnit unit)
 * 延迟一定时间后执行Callable任务;
 * <p>
 * scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
 * 延迟一定时间后,以间隔period时间的频率周期性地执行任务;
 * <p>
 * scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit)
 * 与scheduleAtFixedRate()方法很类似
 * 但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔
 * 而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,
 * 也就是这一些任务系列的触发时间都是可预知的。
 * <p>
 * ScheduledExecutorService功能强大,对于定时执行的任务,建议多采用该方法。
 */
@Test
public void scheduleThreadPoolTest() {
    // 创建指定核心线程数,但最大线程数是Integer.MAX_VALUE的可定时执行或周期执行任务的线程池
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5, new testThreadPoolFactory("scheduledThread"));

    // 定时执行一次的任务,延迟1s后执行
    executorService.schedule(new Runnable() {
        @Override
        public void run() {
            print("scheduleThreadPool");
            System.out.println(Thread.currentThread().getName() + ", delay 1s");
        }
    }, 1, TimeUnit.SECONDS);


    // 周期性地执行任务,延迟2s后,每3s一次地周期性执行任务
    executorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ", every 3s");
        }
    }, 2, 3, TimeUnit.SECONDS);


    executorService.scheduleWithFixedDelay(new Runnable() {
        @Override
        public void run() {
            long start = new Date().getTime();
            System.out.println("scheduleWithFixedDelay 开始执行时间:" +
                    DateFormat.getTimeInstance().format(new Date()));
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long end = new Date().getTime();
            System.out.println("scheduleWithFixedDelay执行花费时间=" + (end - start) / 1000 + "m");
            System.out.println("scheduleWithFixedDelay执行完成时间:"
                    + DateFormat.getTimeInstance().format(new Date()));
            System.out.println("======================================");
        }
    }, 1, 2, TimeUnit.SECONDS);

}


newSingleThreadExecutor:

创建一个单线程化的线程池,它只有一个线程,用仅有的一个线程来执行任务,保证所有的任务按照指定顺序(FIFO,LIFO,优先级)执行,所有的任务都保存在队列LinkedBlockingQueue中,等待唯一的单线程来执行任务。

/**
 * 创建只有一个线程的线程池测试
 * 该方法无参数,所有任务都保存队列LinkedBlockingQueue中,核心线程数为1,线程空闲时间为0
 * 等待唯一的单线程来执行任务,并保证所有任务按照指定顺序(FIFO或优先级)执行
 */
@Test
public void singleThreadPoolTest() {
    // 创建仅有单个线程的线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor(new testThreadPoolFactory("singleThreadPool"));
    for (int i = 0; i < 10; i++) {
        executorService.submit(() -> {
                    print("singleThreadPool");
                    System.out.println(Thread.currentThread().getName());
                }
        );
    }

}


线程池执行流程

由于线程池是以懒加载的方式进行创建线程的,所以线程池创建第一个线程的时候,它普通的创建线程没有区别。

当任务来临时,它会先去判断一下实际线程数是否大于核心线程数,如果小于等于核心线程数,说明此时任务比较少,所以此时只需要创建一个新的线程来执行任务就完事了。
如果实际线程数大于核心线程数了,然后再去判断任务队列有没有满,如果队列没有满,那么就将新任务加入到任务队列中排队等待执行。
如果队列满了,那么再去判断实际线程数是否超过最大线程数,如果没有超过,那么创建一个新的线程来执行任务就完事了。
如果已经超过最大线程数了,那么就会执行拒绝策略。


线程池监控

利用线程池提供的参数进行监控,参数如下:

taskCount:线程池需要执行的任务数量。
completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
largestPoolSize:线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
getActiveCount:获取活动的线程数。
通过扩展线程池进行监控:继承线程池并重写线程池的beforeExecute(),afterExecute()和terminated()方法,可以在任务执行前、后和线程池关闭前自定义行为。如监控任务的平均执行时间,最大执行时间和最小执行时间等。

判断线程池中的任务是否执行完成 

① getCompletedTaskCount() 方式

使用 getCompletedTaskCount() 统计已经执行完的任务,使用 getTaskCount() 获取线程池的总任务,将二者进行对比,如果相等就说明线程池的任务执行完了,反之没有执行完。

private static void isCompleteByTaskCount(ThreadPoolExecutor threadPool) {
    while(threadPool.getCompletedTaskCount()
            != threadPool.getTaskCount()) {
        // 如果 已完成任务数 != 总任务数, 就一直进行自旋
    }
}

这种方式的缺点:

如果已完成任务数 != 总任务数,那么就会一直自旋,过于消耗系统性能;
判断不够精准,因为线程池是公用的,如果这个时候有其他线程来添加了新的任务,那么总的任务数就变了,而我只想判断属于我的这几个任务知否执行完。
所以 getCompletedTaskCount() 并不是最优的实现方式。

② FutureTask 的方式

使用 FutureTask 等待所有任务执行完。(类似于 Thread 里面的 join)

public static void main(String[] args) throws ExecutionException,
             InterruptedException {
    // 创建固定大小的线程池
    ExecutorService executor = Executors.newFixedThreadPool(3);
    // 创建 3 个任务
    FutureTask<Integer> task1 = new FutureTask<>(() -> {
        System.out.println("task1 开始执行");
        Thread.sleep(2000);
        System.out.println("task1 执行结束");
        return 1;
    });
    FutureTask<Integer> task2 = new FutureTask<>(() -> {
        System.out.println("task2 开始执行");
        Thread.sleep(2000);
        System.out.println("task2 执行结束");
        return 2;
    });
    FutureTask<Integer> task3 = new FutureTask<>(() -> {
        System.out.println("task3 开始执行");
        Thread.sleep(2000);
        System.out.println("task3 执行结束");
        return 3;
    });
    // 提交 3 个任务给线程池
    executor.submit(task1);
    executor.submit(task2);
    executor.submit(task3);
    // 等所有任务执行完毕并获取结果 (同步阻塞)
    int ret1 = task1.get();
    int ret2 = task1.get();
    int ret3 = task1.get();
}

3.使用 CountDownLatch 或 CyclicBarrier

public void runByThread() throws InterruptedException {
    ArrayList<File> files = new ArrayList<>();
    enumFile(INPUT_PATH,files); // 递归枚举出所有 HTML 文件
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    CountDownLatch latch = new CountDownLatch(files.size());
    // 遍历文件,多线程解析文件
    for(File f:files) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("开始解析: " + f.getAbsolutePath());
                parseHTML(f); // 解析文件
                latch.countDown(); // 调用一次这个方法表示一个线程已经执行完了
            }
        });
    }
    latch.await(); // 阻塞(死等) -> 直到所有的线程都调用了 countDown()
    // 执行后续的业务逻辑
    System.out.println("索引制作完毕!");
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值