java并发学习1---ThreadpoolExecutor

开发过程中,合理地使用线程池可以带来3个好处:

降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。

提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

1 线程池的创建

ThreadPoolExecutor有以下四个构造方法

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue)
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler)
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory)
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
这里面需要几个参数

1 corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

2 workQueue(任务队列) : 用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:

ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,按FIFO原则进行排序
LinkedBlockingQueue:一个基于链表结构的阻塞队列,吞吐量高于ArrayBlockingQueue。静态工厂方法Excutors.newFixedThreadPool()使用了这个队列
SynchronousQueue: 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量高于LinkedBlockingQueue,静态工厂方法Excutors.newCachedThreadPool()使用了这个队列
PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
3 maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没用了。
4 threadFactory(线程工厂):可以通过线程工厂为每个创建出来的线程设置更有意义的名字,如开源框架guava
5 RejectedExecutionHandler (饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略还处理新提交的任务。它可以有如下四个选项:
AbortPolicy:直接抛出异常,默认情况下采用这种策略
CallerRunsPolicy:只用调用者所在线程来运行任务
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
DiscardPolicy:不处理,丢弃掉
更多的时候,我们应该通过实现RejectedExecutionHandler 接口来自定义策略,比如记录日志或持久化存储等。
6 keepAliveTime(线程活动时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程利用率。

7 TimeUnit(线程活动时间的单位):可选的单位有天(Days)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

2 提交任务

可以使用execute和submit两个方法向线程池提交任务

(1)execute方法用于提交不需要返回值的任务,利用这种方式提交的任务无法得知是否正常执行

threadPoolExecutor.execute(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

(2) submit方法用于提交一个任务并带有返回值,这个方法将返回一个Future类型对象。可以通过这个返回对象判断任务是否执行成功,并且可以通过future.get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成。
Future

static BlockingQueue blockingQueue=new ArrayBlockingQueue<>(10);

    static ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10, 20, 1, TimeUnit.MINUTES, blockingQueue);

(2)另外构造了一个实现Runable接口的类TaskWithoutResult,其逻辑很简单,睡眠1秒
/**
* 无返回值的任务

* @author songxu
 *
 */
class TaskWithoutResult implements Runnable
{
    private int sleepTime=1000;//默认睡眠时间1s
    public TaskWithoutResult(int sleepTime)
    {
        this.sleepTime=sleepTime;
    }
    @Override
    public void run() 
    {
        System.out.println("线程"+Thread.currentThread()+"开始运行");
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {//捕捉中断异常

            System.out.println("线程"+Thread.currentThread()+"被中断");
        }
        System.out.println("线程"+Thread.currentThread()+"结束运行");
    }



}

(3)验证

/**
     * 中断测试
     */
    public static void  test1()
    {
        for(int i=0;i<10;i++)
        {
            Runnable runnable=new TaskWithoutResult(1000);
            threadPoolExecutor.submit(runnable);
        }
        //threadPoolExecutor.shutdown();//不会触发中断
        threadPoolExecutor.shutdownNow();//会触发中断
    }

分别测试shutdown和shutdownNow()方法,结果shutdown()方法的调用并不会引发中断,而shutdownNow()方法则会引发中断。这也正验证前面所说的,shutdown方法只是发出了停止信号,等所有线程执行完毕会关闭线程池;而shutdownNow则是立即停止所有任务。

2 示例2 验证线程池的扩容

在本例中想要验证线程池扩容到核心数量,然后再扩容到最大数量,最后再缩小到核心数量的过程。

(1)首先构造一个线程池,用ArrayBlockingQueue作为其等待队列,队列初始化容量为1。该线程池核心容量为 10,最大容量为20,线程存活时间为1分钟。

static BlockingQueue blockingQueue=new ArrayBlockingQueue<>(1);

    static ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10, 20, 1, TimeUnit.MINUTES, blockingQueue);

(2)另外构造了一个实现Runable接口的类TaskBusyWithoutResult类,其模拟一个繁忙的任务

class TaskBusyWithoutResult implements Runnable
{
    public TaskBusyWithoutResult()
    {
    }
    @Override
    public void run() 
    {
        System.out.println("线程"+Thread.currentThread()+"开始运行");
        int i=10000*10000;
        while(i>0)
        {
            i--;
        }
        System.out.println("线程"+Thread.currentThread()+"运行结束");
    }



}

(3)测试验证,向线程池提交20个任务
/**
* 扩容测试

     */
    public static void  test2()
    {
        for(int i=0;i<20;i++)
        {
            Runnable runnable=new TaskBusyWithoutResult();
            threadPoolExecutor.submit(runnable);
        }
    }

(4)验证结果

在VisualVM中观察线程的变化,在任务提交的瞬间,线程池完成了预热到扩容到最大线程,之所以这么迅速是因为本例中的等待队列长度只有1,可以适当地增加队列长度,但不并不一定能看到扩大最大容量,其原因将在下一节中讲到。在线程池中任务都运行完毕后,可以看到线程池回收了多余的线程,但并没有完全回收,而是保持在核心线程数量。从这里也可以看出,合理地设置核心线程的数量可以减少线程的频繁创建和回收,而这才是线程池的真正作用。

猜你喜欢

转载自blog.csdn.net/liu6219364/article/details/82320812