Java 多线程 系列文章目录:

为什么要使用线程池?

在 Android 开发中,一些耗时的操作都会放到后台线程去执行,比如:网络、本地文件、数据库等。如:

new Thread(new Runnable() {
        @Override
        public void run() {
            //do something...
        }
    }).start();

每个线程至少消耗64k的内存,如果你在某个时间点,迅速开启了很多线程(比如加载列表图片,然后用户滑动列表),这个时候可能内存使用量就会飙升。可能会产生如下问题:

  • 会出现内存抖动(memory churn),因为短时间开启了很多线程,完成任务后,这些线程都会被回收。内存表现为:低-高-低。甚至可能出现OOM。
  • 一个系统所能处理的线程数量是有限的,如果超多了最大承载量,性能会受到很大的影响。而且可能还会影响用户的后续操作。

 

这时候Thread Pool线程池的作用就凸显出来了。

 

ThreadPoolExecutor 详解

Java 为我们提供了操作线程池的API: ThreadPoolExecutor ,该类实现了 ExecutorService 接口

JDK 中相关的线程池的类都实现了该接口。

创建一个线程池可以通过 ThreadPoolExecutor 类来实现:

ThreadPoolExecutor pool = new ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long 

//新建一个线程池
keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);

//执行任务
pool.execute(Runnable);

下面是官方对 ThreadPoolExecutor 的参数说明:

Parameters:
	corePoolSize - the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set
	maximumPoolSize - the maximum number of threads to allow in the pool
	keepAliveTime - when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
	unit - the time unit for the keepAliveTime argument
	workQueue - the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.

下面对各个参数进行个解释:

  • corePoolSize 核心线程数,核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理。核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。
  • maxPoolSize 线程池允许最大的线程数量。
  • keepAliveTime 当线程空闲时间达到 keepAliveTime,该线程会退出,直到线程数量等于 corePoolSize。如果allowCoreThreadTimeout 设置为 true,则所有线程均会退出直到线程数量为0。
  • allowCoreThreadTimeout 是否允许核心线程空闲keepAliveTime退出,默认值为false。
  • workQueue 任务队列。pool.execute(runnable) 提交的 task都 会放到workQueue。
     

下面来一个简单的 sample:

public class MyClass {

    private ThreadPoolExecutor pool ;

    private MyClass(){
    	//创建线程池
        pool = new ThreadPoolExecutor(4, 7, 60, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        for (int i = 0; i < 10; i++) {
        	//提交任务
            myClass.pool.execute(new MyRunnable(myClass));
        }
        myClass.pool.shutdown();
    }

    private String getCount() {
        return pool.getCorePoolSize()+"-"+pool.getActiveCount() + "-" + pool.getMaximumPoolSize();
    }

    private static class MyRunnable implements Runnable {
        MyClass myClass;

        MyRunnable(MyClass myClass) {
            this.myClass = myClass;
        }

        @Override
        public void run() {
            System.out.println("thread name:" + Thread.currentThread().getName() + " start " + myClass.getCount());
            try {
            	//模拟耗时任务
                Thread.sleep(3000L);
                System.out.println("thread name:" + Thread.currentThread().getName() + " end " + myClass.getCount());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

上面的代码很简单:创建了一个 corePoolSize 为 4,maxPoolSize 为 7 的线程池。
然后往线程池里提交 10 个任务,每个任务打印 :

pool.getCorePoolSize()+"-"+pool.getActiveCount() + “-” + pool.getMaximumPoolSize()

即 corePoolSize (核心线程数),activeCount (正在活动的线程总数) 和 maximumPoolSize (线程池允许的最大线程数) 值。
 

测试结果如下:

thread name:pool-1-thread-1 start 4-2-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-3 start 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-1 end 4-4-7
thread name:pool-1-thread-2 end 4-3-7
thread name:pool-1-thread-4 end 4-4-7
thread name:pool-1-thread-1 start 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-3 end 4-4-7
thread name:pool-1-thread-3 start 4-4-7
thread name:pool-1-thread-2 end 4-4-7
thread name:pool-1-thread-4 end 4-4-7
thread name:pool-1-thread-3 end 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-1 end 4-3-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-4 end 4-2-7
thread name:pool-1-thread-2 end 4-2-7

Process finished with exit code 0

从测试结果来看,我们打印 pool.getCorePoolSize()+"-"+pool.getActiveCount() + “-” + pool.getMaximumPoolSize() 的值是正常的,但是只创建了4个线程:

pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4

我们设置了线程池的最大数为 7,我们提交了 10 个任务,但是为什么只创建了 corePoolSize = 4 个线程?

查看官方文档可以找到答案:

  • 当通过 execute(Runnable) 提交一个新任务,并且小于 corePoolSize 正在运行的线程数,将会创建一个新的线程来处理这个任务,不管线程池里有没有空闲的线程。
  • If there are more than corePoolSize but less than maximumPoolSize threads running, a new thread will be created only if the queue is full.  意思是说:任务数量大于 corePoolSize,但是小于 maximumPoolSize,并且 workQueue 队列满了,才会创建新的线程。
  • 如果 corePoolSize 和 maximumPoolSize 值设置成一样的,相当于创建了一个固定数量的线程池。
  • 多数情况下,都是通过构造方法来设置 corePoolSize 和 maximumPoolSize,但是也可以通过 setCorePoolSize 和setMaximumPoolSize 来动态设置。

所以上面的例子,只创建了 4 个线程,因为虽然我们提交了 10 个任务,但是构建 workQueue 时候没有传入队列大小,默认大小是 Integer.MAX_VALUE,所以 workQueue 并没有满,所以最多就创建了4个线程。

 

据此,我们可以把 workQueue 队列容量改成 4 :

pool = new ThreadPoolExecutor(4, 7, 60, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(4));

测试结果如下所示:

thread name:pool-1-thread-1 start 4-2-7
thread name:pool-1-thread-2 start 4-2-7
thread name:pool-1-thread-3 start 4-3-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-5 start 4-6-7
thread name:pool-1-thread-6 start 4-6-7
thread name:pool-1-thread-1 end 4-6-7
thread name:pool-1-thread-2 end 4-6-7
thread name:pool-1-thread-2 start 4-5-7
thread name:pool-1-thread-1 start 4-6-7
thread name:pool-1-thread-3 end 4-6-7
thread name:pool-1-thread-3 start 4-6-7
thread name:pool-1-thread-4 end 4-6-7
thread name:pool-1-thread-5 end 4-6-7
thread name:pool-1-thread-4 start 4-6-7
thread name:pool-1-thread-6 end 4-5-7
thread name:pool-1-thread-1 end 4-4-7
thread name:pool-1-thread-2 end 4-4-7
thread name:pool-1-thread-3 end 4-2-7
thread name:pool-1-thread-4 end 4-1-7

Process finished with exit code 0

发现创建了 6 个线程,大于上一次的测试结果(上一次是创建了 4 个线程),可是我们设置的 maximumPoolSize = 7,按道理应该是创建 7 个线程才对呀,这是为什么呢?

这需要了解下 workQueue 队列的策略了。我们上面的列子使用的是 LinkedBlockingQueue。

下面来看看官方文档对 BlockingQueue的描述:

Any link BlockingQueue may be used to transfer and hold
submitted tasks.  The use of this queue interacts with pool sizing:
If fewer than corePoolSize threads are running, the Executor
always prefers adding a new thread
rather than queuing.

If corePoolSize or more threads are running, the Executor
always prefers queuing a request rather than adding a new
thread.

If a request cannot be queued, a new thread is created unless
this would exceed maximumPoolSize, in which case, the task will be
rejected.

主要意思就是:

  • 如果运行的线程少于 corePoolSize,Executor 会创建新线程来执行任务,不会把任务放进 queue。
  • 如果运行的线程等于或多于 corePoolSize,Executor 将请求加入队列,而不是创建新的线程。
  • 如果队列已满,无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

这样就能解释为什么只创建 6 个线程了。

总共有 10 个 task,核心线程 corePoolSize = 4,所以 3 个任务是不会放进 queue 的,直接创建 3 个新线程来处理 task 了,然后再执行 execute(Runnable) 的时候,就会大于等于 corePoolSize,所以就会把接下来的 4 个任务放进 queue (容量为4),然后就剩下 3 个 task 了,发现队列已经满了,创建 3 个线程来处理这剩下的 3 个 task,所以总共只创建 6 个线程了。

 

maximumPoolSize = 7,我就是想让它创建 7 个线程怎么办?我们知道了上面的原理就很简单了,可以把队列的最大容量改成 3 或者添加 11 个任务就可以了:

new LinkedBlockingQueue<Runnable>(3)

或者:

for (int i = 0; i < 11; i++) {
    myClass.pool.execute(new MyRunnable(myClass));
}

运行结果如下所示:

thread name:pool-1-thread-1 start 0-1-7
thread name:pool-1-thread-2 start 0-2-7
thread name:pool-1-thread-3 start 1-4-7
thread name:pool-1-thread-4 start 4-5-7
thread name:pool-1-thread-5 start 4-7-7
thread name:pool-1-thread-6 start 4-7-7
thread name:pool-1-thread-7 start 4-7-7
.....

 

Java 提供了 3 种策略的 Queue:

  • SynchronousQueue 直接传送task(Direct handoffs)
  • LinkedBlockingQueue 无边界队列(Unbounded queues)
  • ArrayBlockingQueue 有边界队列(Bounded queues)
     

JDK 提供的工厂类 Executors 来创建线程池

Java 也给我们提供了一些工厂方法来创建线程池:

  • Executors.newFixedThreadPool(int threads);
  • Executors.newCachedThreadPool();
  • Executors.newSingleThreadExecutor();
  • Executors.newScheduledThreadPool(int threads);

这些方法都是通过构建 ThreadPoolExecutor 来实现的,具体的细节可以去看看对应的实现,我们已经对 ThreadPoolExecutor  的构造函数的参数有了比较好的了解,在使用这些工具方法创建线程池时,一定要看看它里面是怎么实现的。如果都不满足你的需求,可以自己构造 ThreadPoolExecutor。

 

 

 

GitHub 加速计划 / th / ThreadPool
7.74 K
2.22 K
下载
A simple C++11 Thread Pool implementation
最近提交(Master分支:2 个月前 )
9a42ec13 - 9 年前
fcc91415 - 9 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐