JAVA线程池ThreadPoolExecutor详解

摘要

说起进程大家都不陌生,学过操作系统的肯定都知道。而为了不使进程由于某种不当操作而陷入阻塞,进程就出现了。在一个进程中可以创建线程,线程是比进程更小的一个粒度。我们都知道通过继承Thread类,或者实现runnable接口来创建一个线程,然而在进程中创建和销毁线程都会消耗大量的时间,而且大量的子线程会分享主线程的系统资源,从而会使主线程因资源受限而导致应用性能降低为了减少线程在创建和销毁的过程中所消耗的时间,为了解决这些问题,线程池就由此而生。

线程池的工作原理

首先看一个例子:

假设在一台服务器完成一项任务的时间为T
T1 创建线程的时间
T2 在线程中执行任务的时间,包括线程间同步所需时间
T3 线程销毁的时间
那么T = T1+T2+T3。

分析一下:
T1,T3是多线程本身的带来的开销,希望减少T1,T3所用的时间,从而减少T的时间。如果在程序中频繁的创建或销毁线程,这导致T1和T3在T中占有相当比例。显然这是突出了线程的弱点(T1,T3),而不是优点(并发性)。线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的

并且线程的创建并不仅仅是我们new一个Thread那么简单。当JVM把创建线程的任务交给OS时,OS创建一个线程的代价是非常昂贵的,它需要给这个线程分配内存,列入调度,在线程切换的时候还要进行内存换页,清空cpu缓存,切换回来的时候还要重新从内存中读取信息,破坏数据的局部性。
然后就看一下我们的线程池吧
在java.util.concurrent包中,有这样一个类ThreadPoolExecutor,查看他的继承关系我们可以发现
这里写图片描述

  • Executor:接口 线程池必须要实现的接口 里面只有一个方法 就是execute方法 。提交任务
  • ExecutorService:接口 继承Executor接口 增加了submit提交任务的方式(有返回值) 以及关闭 操作
  • AbstractExecutorService实现了ExecutorService的所有方法
    ThreadPoolExecutor就是我们使用的线程池类了

而我们通常新建一个线程池会使用,Executors的静态方法来创建几个常用的线程池。而这几个常用的线程池基本都是通过新建一个ThreadPoolExecutor方法实现的。
所以在介绍使用Executors线程池之前,先介绍一个ThreadPoolExecutor的构造方法

构造方法

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
变量 意义
corePoolSize 核心线程池大小
maximumPoolSize 最大线程池大小
keepAliveTime 空闲线程的存活时间
unit 空闲线程保持存活的时间的单位
workQueue 阻塞任务队列,当要执行的任务超出corePoolSize ,那么此时将把任务放入队列中。
threadFactory 创建线程使用的工厂
handler 提交任务超出maxmumPoolSize+workQueue时的拒绝策略

存活时间(keepAliveTime)

keepAliveTime即空闲线程(大于corePoolSize 小于maximumPoolSize 的线程)保持存活的时间,超出这个时间,线程将被销毁。如果设置了allowCoreThreadTimeOut(true),那么核心线程在超出空闲时间时也会被销毁

存活的时间单位u(unit)

空闲线程存活的时间单位,可选择的有

TimeUnit.NANOSECONDS		//		千分之一微秒(纳秒)
TimeUnit.MICROSECONDS		//		千分之一毫秒(微妙)
TimeUnit.MILLISECONDS		//		千分之一秒(毫秒)
TimeUnit.SECONDS			//		秒		
TimeUnit.MINUTES			//		分
TimeUnit.HOURS				//		小时
TimeUnit.DAYS				//		天

任务队列(workQueue)###

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO (先进先出) 排序元素,吞吐量通常要高于 ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool() 使用了这个队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool 使用了这个队列。
  • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

线程工厂(threadFactory)###

每当线程需要创建一个线程时,都是通过线程工厂方法。默认的线程工厂方法将创建一个新的、非守护的线程,并且不包含特殊的配置信息。通过定制一个线程工厂,可以定制线程池的配置信息。比如当你希望为线程池中的线程维护一些统计信息(包含有多少个线程被创建和销毁)以及在线程被创建或者终止时调试消息写入日志,都可以自定义线程工厂。

拒绝策略(handler)###

当有界队列被填满后,拒绝策略开始发挥作用、JDK提供了集中不同的RejectedExecutionHandler 实现,每种实现都有不同的饱和策略。

  • AbortPolicy 中止策略 :默认的饱和策略,该策略将抛出为检查的RejectedExecutionException.调用者可以捕获这个异常,然后根据需求编写自己的代码。
  • DiscardPolicy 抛弃策略: 当新提交的任务无法保存到队列中等待执行时,抛弃策略会悄悄抛弃该任务
  • DiscardOldestPolicy 抛弃最旧的策略: 抛弃下一个将被执行的任务,然后添加新提交的任务
  • CallerRunsPolicy 调用者运行策略: 该策略实现了一种调用机制,该策略既不抛弃任务,也不抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务(一般该调用者是main线程)

任务执行的基本流程###

这里写图片描述
查看源码后我们可以发现线程池任务的执行流程:

  • 当线程池中的线程数小于corePoolSize 时,新提交的任务直接新建一个线程执行任务(不管是否有空闲线程)
  • 当线程池中的线程数等于corePoolSize 时,新提交的任务将会进入阻塞队列(workQueue)中,等待线程的调度
  • 当阻塞队列满了以后,如果corePoolSize < maximumPoolSize ,则新提交的任务会新建线程执行任务,直至线程数达到maximumPoolSize
  • 当线程数达到maximumPoolSize 时,新提交的任务会由(饱和策略)管理

在这里简单的测试一下CallerRunsPolicy 策略
我们新建了一个核心线程大小和最大线程大小都为1的线程池,然后设置其拒绝策略为CallerRunsPolicy,然后输出任务执行的线程名称。

/**
 *
 * @author xiaosuda
 * @date 2018/1/8
 */
public class CallerRunsPolicyTest {

    public static void main(String [] args){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1), new ThreadPoolExecutor.CallerRunsPolicy());
        for (int i = 0; i < 3; i++) {
            threadPoolExecutor.execute(() -> {
                try {
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        threadPoolExecutor.shutdown();
    }
}

输出结果:

pool-1-thread-1
main
pool-1-thread-1

分析一下:因为核心线程池+ 阻塞队列的大小=2,而我们新建了3个任务。当提交第一个任务时,新建线程执行任务,当提交第二个任务时,由于线程池的核心大小为1并且有任务在执行,放入阻塞队列,当提交第三个任务,发现阻塞队列已经满了,而且线程池的最大的线程个数也达到了最大。所以交给拒绝策略来处理,而CallerRunsPolicy拒绝策略会将任务回退到调用者(main线程),从而降低新任务的流量。
###向线程池提交任务###
####execute####
我们可以使用 void execute(Runnable command)方法向线程池提交任务,但是该方法没有返回值

 ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
 cachedThreadPool.execute(new Runnable() {
             @Override
             public void run() {
                 //do some things
             }
         });

submit####

我们也可以使用 Future submit(Callable task)方法来提交任务。它会返回一个future对象,我们可以通过future.get来获得返回值,get方法会阻塞直到线程执行成功或者抛出异常。我们也可以使用 get(long timeout, TimeUnit unit) 来阻塞一段时间后立即返回,这时候可能任务还没有执行成功。使用场景:比如我们在使用高德地图来查找去某个地方的路线时(路线可能有多种,但是在计算路线时耗费的时间不同),我们可以设置一个超时时间,只显示那些已经计算完成的路线。
举个例子,我们要得到一个100以内的随机数。

 ExecutorService executorService = Executors.newSingleThreadExecutor();

        Future<Integer> randomNum = executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return new Random().nextInt(100);
            }
        });
        try {
            System.out.println(randomNum.get());
        } catch (InterruptedException e) {
            //中断异常
            e.printStackTrace();
        } catch (ExecutionException e) {
            // 任务异常
            e.printStackTrace();
        } finally {
            //关闭线程池
            executorService.shutdown();
        }

关闭线程池###

在ThreadPoolExecutor 中提供了关闭线程池的两种方法。

  • shutdown
  • shutdownNow
    查看源码后我们可以发现

shutdown:将线程池的状态修改为SHUTDOWN,此时无法向线程池中添加任务,否则会由拒绝策略来处理添加的任务。但是已经添加的任务(任务队列中的任务和正在执行的任务)会继续执行直到完成,然后线程池退出。
shutdownNow:将线程池的状态修改为STOP,此时无法向线程池添加任务,否则会由拒绝策略来处理添加的任务。并且任务队列中的任务也不再执行,只执行那些正在执行的任务,并且试图中断它们,然后返回那些未执行的任务,线程池退出。

/**
 *
 * @author xiaosuda
 * @date 2018/1/8
 */
public class ShutdownNowTest {

    public static void main(String [] args){
        Integer threadNum = Runtime.getRuntime().availableProcessors();
        ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder();
        threadFactoryBuilder.setNameFormat("ShutdownNowTestPool");
        ThreadFactory threadFactory = threadFactoryBuilder.build();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, threadNum, 60L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5), threadFactory, new ThreadPoolExecutor.AbortPolicy());

        for (int i = 0; i < 10; i++) {
            String content = "Thread-name" + i;
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    Thread.currentThread().setName(content);
                    int x = 10000;
                    //为了模拟长时间任务,避免任务队列中没有任务
                    while (x-- > 0) {

                    }
                    System.out.println( Thread.currentThread().getName());
                }
            });
        }

        List<Runnable> runnables = threadPoolExecutor.shutdownNow();
        System.out.println("--------------------------------未执行的任务--------------------------------");
        for (Runnable runnable : runnables) {
            new Thread(runnable).start();
        }
    }
}

执行结果:

Thread-name0
Thread-name6
Thread-name1
Thread-name2
Thread-name3
Thread-name7
--------------------------------未执行的任务--------------------------------
Thread-name8
Thread-name4
Thread-name5
Thread-name9

Executors框架###

在Executors,jdk为我们提供了几种常用的线程池

  • newFixedThreadPool:此时核心线程池数量等于线程池最大数量。 将创建一个固定长度的线程池,阻塞队列使用的是LinkedBlockingQueue,线程可以重用。
  • newCachedThreadPool: 创建一个可以缓存的线程池,默认的核心线程池数量为0,最大线程池数量为Integer.MAX_VALUE.阻塞队列使用的是SynchronousQueue。线程的存活时间为60s.一旦有任务提交就新建一个线程执行任务,如果有空闲线程超出60s自动回收,当需求大量增加,且任务的执行时间较长时,容易oom,此时可以使用信号量来控制同时执行线程的个数。
  • newSingleThreadExecutor: 创建一个核心线程和最大线程数都为1的线程池,阻塞队列为LinkedBlockingQueue。串行执行任务,此时如果在线程中为线程池添加任务要注意避免死锁的情况发生。
  • newScheduledThreadPool:创建一个固定长度的线程池,而且以延迟或者定时的方式执行任务。

newCachedThreadPool造成的oom####

新建了一个CachedThreadPool,添加任务时睡眠100毫秒,以到达执行长时间任务的目地。循环地向CachedThreadPool中添加任务。

/**
 *
 * @author xiaosuda
 * @date 2018/1/3
 */
public class TestExecutorsThread {

    public static void main(String [] args){
        cacheThreadPool();
    }
    private static void cacheThreadPool() {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            cachedThreadPool.execute(() ->{
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            });
        }
        cachedThreadPool.shutdown();
    }
}

执行结果后发现OOM:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
	at java.lang.Thread.start0(Native Method)
	at java.lang.Thread.start(Thread.java:717)
	at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1368)
	at com.dfire.Thread.TestExecutorsThread.cacheThreadPool(TestExecutorsThread.java:19)
	at com.dfire.Thread.TestExecutorsThread.main(TestExecutorsThread.java:14)

所以我们在使用newCachedThreadPool时应该注意避免添加长时间的任务,或者限制并发执行的任务的数量。

newSingleThreadExecutor造成的死锁####

由于newSingleThreadExecutor创建的是单一的线程,任务都在串行执行,如果任务间产生依赖,那么就容易发生死锁。下面举个newSingleThreadExecutor死锁例子。

/**
 *
 * @author xiaosuda
 * @date 2018/1/8
 */
public class SingleThreadExecutorTest {

    public static void main(String [] args) throws ExecutionException, InterruptedException {
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        Random random = new Random();
        Future<Integer> randomSum = singleThreadExecutor.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                Future<Integer> randomOne = singleThreadExecutor.submit(new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return random.nextInt(100);
                    }
                });
                Future<Integer> randomTwo = singleThreadExecutor.submit(new Callable<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return random.nextInt(100);
                    }
                });
                return randomOne.get() + randomTwo.get();
            }
        });
        System.out.println(randomSum.get());
    }
}

程序一直在执行,但没有结果输出,通过 jstack -l [pid] ->thread.jstack 命令分析
打开thread.jstack后会发现:

2018-01-08 12:38:52
Full thread dump Java HotSpot(TM) Client VM (25.131-b11 mixed mode):

"pool-1-thread-1" #11 prio=5 os_prio=0 tid=0x14e47c00 nid=0x1278 waiting on condition [0x155ff000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x04750dd0> (a java.util.concurrent.FutureTask)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.FutureTask.awaitDone(FutureTask.java:429)
	at java.util.concurrent.FutureTask.get(FutureTask.java:191)
	at com.dfire.Thread.SingleThreadExecutorTest$1.call(SingleThreadExecutorTest.java:31)
	at com.dfire.Thread.SingleThreadExecutorTest$1.call(SingleThreadExecutorTest.java:16)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- <0x04742ca0> (a java.util.concurrent.ThreadPoolExecutor$Worker)

线程发生了死锁。

闭锁###

闭锁时一种同步工具类,可以延迟线程的进度直到达到终止状态,闭锁就相当于一扇门,在闭锁到达结束状态之前,这扇门一直时关闭的,并且没有任何线程能通过。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态,闭锁可以用来确保某些活动知道其它活动都完成后才继续执行。
CountDownLatch就是一种灵活的闭锁实现。
比如:一个简单的zookeeper连接。

/**
 * @author xiaosuda
 * @date 2018/01/08
 */
public class ConnectionWatcher implements Watcher {


    private static final int SESSION_TIMEOUT = 5000;
    protected ZooKeeper zooKeeper;
    private CountDownLatch connectedSignal = new CountDownLatch(1);


    public void connect(String hosts) throws IOException, InterruptedException {
        zooKeeper = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
        connectedSignal.await();
    }

    @Override
    public void process(WatchedEvent watchedEvent) {
        if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
            connectedSignal.countDown();
            System.out.println("连接成功");
        }
    }

    public void close() throws InterruptedException {
        zooKeeper.close();
        System.out.println("连接关闭");
    }

    public static void main(String [] args) throws IOException, InterruptedException, KeeperException {
        ConnectionWatcher connectionWatcher = new ConnectionWatcher();
        connectionWatcher.connect("117.88.151.70:2181");
        connectionWatcher.close();
    }
}

在这里我们使用了CountDownLatch,并且计数器的初始值为1。在connect方法中,我们使用connectedSignal.await()进入等待状态。ConnectionWatcher类实现了Watcher接口,别且重写了process方法。在process方法中,如果连接成功就使计数器的值-1,将等待状态变为结束。开始执行连接成功的后一些操作。
还有FutureTask也可以用做闭锁(FutureTask实现了Future语义,表示一种抽象的可生成结果的计算)。Future.get的行为取决于任务的状态,如果任务已经完成,那么get会立即返回结果,否则将阻塞知道任务进入完成状态,然后返回结果或者抛出异常。

信号量###

信号量(Semaphore)用来控制同时访问某个特定资源的操作数量,或者同时执行某定操作的数量,信号量还可以用来实现某种资源池,或者对容器施加边界。
信号量有两个常用的方法:
Semaphore管理着一组虚拟的许可,许可的初始状态可以通过构造函数来执行,在执行操作时可以首先获得许可,并在使用以后释放许可。如果没有许可,那么semaphore.acquire()将阻塞直到有许可。semaphore.release()方法将返回一个许可信号量。信号量的一种简化形式是二值信号量,即初始值为1的semaphore。二值信号量可以做互斥体,并不具备不可重入的加锁语义,谁拥有这个唯一的许可,就拥有了互斥锁。在上面的newCachedThreadPool造成的oom中,我们可以使用信号量来控制任务的并发量。修改后的代码如下:

/**
 *
 * @author xiaosuda
 * @date 2018/1/3
 */
public class TestExecutorsThread {
    private static Integer MAX_TASK = 100;
    private static Semaphore semaphore = new Semaphore(MAX_TASK);
    public static void main(String [] args) throws InterruptedException {
        cacheThreadPool();
    }
    private static void cacheThreadPool() throws InterruptedException {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            semaphore.acquire();
            cachedThreadPool.execute(() ->{
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }
        cachedThreadPool.shutdown();
    }
}

执行后发现不会出现OOM,并且最终会全部执行成功。

栅栏###

我们已经看到通过闭锁来启动一组相关的操作,或者等待一组相关的操作结束。闭锁是一次性对象,一旦进入终止状态,就不能被重置。栅栏类似于闭锁,它能阻塞一组线程知道某个事件发生。栅栏与闭锁的区别在于:所有线程必须同时到达栅栏位置,才能继续执行。到线程到达栅栏位置时调用await方法即可。闭锁用于等待事件,而栅栏用于等待线程。栅栏用于实现一些协议,例如:明天3年纪2班的所有学生8:00在学校门口集合,到了以后要等其他人,然后一起做下一步的事情,比如去博物馆。

/**
 *
 * @author xiaosuda
 * @date 2018/1/8
 */
public class CyclicBarrierTest {
    private CyclicBarrier cyclicBarrier = null;
    private ExecutorService cachedThreadPool;
    private Integer studentsNum;


    public CyclicBarrierTest(Integer studentsNum) {
        this.studentsNum = studentsNum;
        cyclicBarrier = new CyclicBarrier(studentsNum, new Runnable() {
            @Override
            public void run() {
                System.out.println("全部同学到齐,一起去博物馆。");
            }
        });

    }

    public void start() {
        cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < studentsNum; i++) {
            cachedThreadPool.execute(new Student("学生" + i));
        }
        cachedThreadPool.shutdown();
    }

    private class Student implements Runnable {
        private String name;

        public Student(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            System.out.println(name + "到了");
            try {
                //等待其它学生的到来
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String [] args){

        CyclicBarrierTest cyclicBarrierTest = new CyclicBarrierTest(10);

        cyclicBarrierTest.start();

    }

}

执行结果:

学生0到了
学生1到了
学生2到了
学生3到了
学生4到了
学生5到了
学生6到了
学生7到了
学生8到了
学生9到了
全部同学到齐,一起去博物馆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章