一篇文章弄懂Java线程池

为社么要使用线程池?

避免系统频繁创建和销毁线程,对创建地线程进行复用。即使用线程时直接从线程池获取空闲线程,关闭线程时将线程退回到线程池。

如何创建一个线程池?

通过Executors进行创建,有以下几种方式:

  • newFixedThreadPool: 返回一个固定线程数量的线程池。
  • newSingleThreadExecutor: 返回只有一个线程的线程池。
  • newCachedThreadPool: 返回一个根据实际情况调整线程数量的线程池。
  • newSingleThreadSchedulerExecutor: 返回一个SchedulerExecutorService对象,线程池大小为1。
  • newSchedulerThreadPool: 返回一个SchedulerExecutorService对象,该线程池可以指定线程数量。

下面用newFixedThreadPool进行测试。

package com.demo1.thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo1 {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        for (int i = 1; i <= 20; i++) {
            executorService.execute(new PrintCurrentTimeTask());
        }
    }

    private static class PrintCurrentTimeTask implements Runnable {

        @Override
        public void run() {
            try {
                System.err.println(Thread.currentThread() + "执行任务");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

上述代码创建了5个线程的线程池,也就是同时只能有5个线程执行。剩余的任务将等待。
一篇文章弄懂Java线程池_第1张图片
newSingleThreadSchedulerExecutor以及newSchedulerThreadPool
最终将创建ScheduledThreadPoolExecutor对象,该类直接继承ThreadPoolExecutor。
下面介绍三个重要的方法:

方法 说明
schedule 延时执行
scheduleAtFixedRate 创建一个周期性任务,第n次执行时间:initialDelay + n * period
scheduleWithFixedDelay 创建一个周期性任务,每一次执行再前一次执行完成后,过period时间执行
package com.demo1.thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Demo1 {
    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
        //延迟5s后执行
        executorService.schedule(new Delay5SecondTask(), 3, TimeUnit.SECONDS);
        //延迟4s执行,周期为2s
        executorService.scheduleWithFixedDelay(new CycleTask(), 4, 2, TimeUnit.SECONDS);
    }

    /**
     * 延迟5s后执行
     */
    private static class Delay5SecondTask implements Runnable {
        @Override
        public void run() {
            System.err.println("------------延迟3s后执行-------------");
        }
    }

    /**
     * 周期执行
     */
    private static class CycleTask implements Runnable {
        @Override
        public void run() {
            System.out.println("start:" + System.currentTimeMillis());
            System.err.println("oooooooooooooo执行任务oooooooooooooo");
            System.out.println("end:" + System.currentTimeMillis());
        }
    }
}

一篇文章弄懂Java线程池_第2张图片
使用scheduleAtFixedRate存在的问题,如果线程执行时间大于周期会发生什么情况?
——答案是会立即直接

下面代码,延迟4s执行,周期为2s,而每个任务需要执行3s

package com.demo1.thread;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Demo1 {
    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
        //延迟4s执行,周期为2s
        executorService.scheduleAtFixedRate(new CycleTask(), 4, 2, TimeUnit.SECONDS);
    }

    /**
     * 执行时间为3s的任务
     */
    private static class CycleTask implements Runnable {
        @Override
        public void run() {
            try {
                System.out.println("start:" + System.currentTimeMillis());
                Thread.sleep(3000);
                System.out.println("end:" + System.currentTimeMillis());
            } catch (InterruptedException e) {}
        }
    }
}

一篇文章弄懂Java线程池_第3张图片

——无论通过哪种方式创建,实际上都是创建一个ThreadPoolExecutor对象(就是线程池)。

创建ThreadPoolExecutor参数详解

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
	/* 省略 */
}
参数 说明
corePoolSize 线程池中线程数量
maximumPoolSize 线程池中最大线程数量
keepAliveTime 线程数量超过corePoolSize,多余空闲线程的存活时间
unit keepAliveTime的时间单位
workQueue 任务队列,存放还未被执行的任务
threadFactory 用于创建线程的线程工厂
handler 拒绝策略
下面介绍三种任务队列

其实还有一个PriorityBlockingQueue:一个具有优先级的无限阻塞队列。(只提一下)
(一)有界任务队列
一篇文章弄懂Java线程池_第4张图片

(二)无界任务队列
一篇文章弄懂Java线程池_第5张图片
使用newFixedThreadPool()创建一个线程池,使用的是无界的任务队列。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

使用newSingleThreadExecutor()创建一个线程池,使用的也是无界的任务队列,与newFixedThreadPool的唯一区别就是使用单线程。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

(三)直接提交队列
一篇文章弄懂Java线程池_第6张图片
使用newCachedThreadPool()创建线程池,使用的是直接提交的队列。可以看到多余的线程将在60s后回收掉。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
下面介绍拒绝策略

JDK内置的四种拒绝策略

拒绝策略 说明
AbortPolicy 直接抛出异常
CallerRunsPolicy 只要线程池未关闭,直接在调用者线程中执行被丢弃的任务
DiscardOldestPolicy 丢弃一个最老的请求,并尝试再次提交该任务
DiscardPolicy 直接丢弃
下面介绍自定义线程工程Factory

我们可以通过自定义线程工程来创建我们想要的线程

package com.demo1.thread;

import java.util.concurrent.*;

public class Demo1 {
    public static void main(String[] args) {
        new ThreadPoolExecutor(5, 5, 0, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(10), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setDaemon(true);
                thread.setPriority(5);
                return new Thread(r);
            }
        });
    }
}
下面介绍如何扩展线程池

重写线程池的一些方法,以达到一些扩展功能,比如打印日志。
shutdown():关闭线程池,等待所有任务执行完后再关闭线程池。

package com.demo1.thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;

public class Demo1 {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.MILLISECONDS,
                new SynchronousQueue<Runnable>()) {
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("执行前调用");
            }

            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                System.out.println("执行后调用");
            }

            @Override
            protected void terminated() {
                System.out.println("线程池关闭");
            }
        };
        for (int i = 0; i < 3; i++) {
            threadPoolExecutor.execute(new PrintCurrentTimeTask());
        }
        threadPoolExecutor.shutdown();
    }

    private static class PrintCurrentTimeTask implements Runnable {

        @Override
        public void run() {
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
        }
    }
}

一篇文章弄懂Java线程池_第7张图片

下面介绍线程池提交任务的两种方法

上面都是使用execute方法进行任务提交,但实际上还可以使用submit进行提交

使用submit会返回一个Future对象,通过该对象我们可以判断任务执行是否成功,并且可以通过Future的get()方法来获取返回值。下面来具体演示submit方法.

get()方法会一直阻塞线程直到执行完毕,支持超时获取。

package com.demo1.thread;

import java.util.concurrent.*;

public class Demo1 {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> future = executorService.submit(new HasReturnTask());
        System.out.println(future.get());
        Future<String> future1 = executorService.submit(new TimeoutTask());
        System.out.println(future1.get(2, TimeUnit.SECONDS));
    }

    private static class HasReturnTask implements Callable<String> {
        @Override
        public String call() throws Exception {
            return "任务执行完毕";
        }
    }

    private static class TimeoutTask implements Callable<String> {
        @Override
        public String call() throws Exception {
            Thread.sleep(3000);
            return "任务执行完毕";
        }
    }
}

在这里插入图片描述

下面介绍关闭线程池的两种方法

前面提到了使用shutdown来关闭线程池。现在提一下另一个关闭线程池的方法shutdownNow
关闭线程池原理:遍历线程池中的工作线程,逐个调用interrupt中断线程,所以无法响应中断的任务可能永远无法中止。

关闭方法 说明
shutdown 将线程池状态设置成SHUTDOWN并中断所有空闲状态线程
shutdownNow 将线程池状态设置成STOP,然后停止所有正在执行及空闲状态线程

通常调用shutdown方法关闭线程,如果不要求任务全部执行完,也可调用shutdownNow完成关闭。

最后介绍如何合理配置线程池

该段文字摘自JAVA并发编程的艺术

类别 详情
任务(进程)的性质 CPU密集型任务、IO密集型任务、混合型任务
任务的优先级 高、中、低
任务的执行时间 长、中、短
任务的依赖性 是否依赖其他系统资源,如数据库连接。
  • 对于CPU密集型任务因配置尽可能小的线程,如 (CPU数 + 1) 个线程
  • 由于IO密集型任务线程并不是一直在执行任务,则配置尽可能多的线程,如 (CPU数 * 2)
  • 对于混合型任务,如果可以拆分,将其拆分为一个CPU密集型和一个IO密集型,只要两个任务执行时间相差不是太大,那么分解后的吞吐量将高于串行执行的吞吐量
  • 优先级不同的任务可以使用优先级队列来处理

可以通过Runtime.getRuntime().availableProcessors()获取CPU数

并发编程新手建议入门书籍
Java并发编程的艺术
实战Java高并发程序设计

你可能感兴趣的:(一篇文章弄懂Java线程池)