java中线程池的实现在jdk1.5以上版本提供了ThreadPoolExecutor类,该类继承了抽象类AbstractExecutorService,是接口
Executor的底层实现类。
那么这里首先了解下Executor。jdk文档中说明了Executor接口执行已提交的 Runnable 任务的对象。此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。通常使用 Executor 而不是显式地创建线程。例如,可能会使用以下方法:
Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
而不是为一组任务中的每个任务调用 new Thread(new(RunnableTask())).start()。由此可见Executor简化了线程的创建,调度、撤销等,降低了线程因创建和撤销而花费的系统开销。
ThreadPoolExecutor的完整构造方法是:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
下面对其参数进行说明:
corePoolSize: 线程池维护线程的正常数目,即线程池的正常大小。
maximumPoolSize:线程池维护线程的最大数量。当线程池满了(即线程池中有corePoolSize个线程了)同时缓冲队列也满了情况下,如果有新的任务来临,可以开辟新的线程来执行任务,但 是总的线程数目不能超过maximumPoolSize
keepAliveTime: 线程池维护线程所允许的空闲时间。如果池中当前有多于 corePoolSize 的线程,则这些多出的线程在空闲时间超过 keepAliveTime 时将会终止。从而减少了资源消耗。
unit: 线程池维护线程所允许的空闲时间的单位。即制定keepAliveTime的时间单位。
workQueue: 线程池所使用的缓冲队列。当有任务来临时,如果当前线程池的活动线程数目大于等于corePoolSize时,同时缓冲队列未满时,则将任务添加到该队列中。
handler: 线程池对拒绝任务的处理策略。所谓拒绝,即当前线程池中活动的线程数目等于maxinumPoolSize,同时队列满时,则线程池对任务拒绝。该处理策略可以自己编写,也可以使用jdk中提供的四种策略,注意这四种策略要慎重选择,选择不当可能导致你不想要的结果。
下面对该四种策略进行介绍:
1、ThreadPoolExecutor.AbortPolicy。该策略为默认的策略。即任务遭到拒绝时抛出运行时异常RejectedExecutionException。
2、ThreadPoolExecutor.CallerRunsPolicy。该策略绕过线程池,直接在添加任务的线程(即调用execute方法的线程,execute方法将在下面降到)执行被拒绝的任务,说实话我还不知道在什么情况下会用到该策略。
3、ThreadPoolExecutor.DiscardPolicy。该策略比较简单,直接丢掉被拒绝的任务。
4、ThreadPoolExecutor.DiscardOldestPolicy。该策略丢掉老任务,保留新任务。具体就是从任务缓冲队列头部删除没有被执行的任务,然后将新到的任务添加到队列中。
了解了它的构造函数以及相关参数以后以后,下面我们来看看怎么将任务添加到线程池的。
当有任务到达时,通过 execute(Runnable)方法被添加到线程池,这里可以看到所谓的任务其实就是实现了接口Runnable的类型对象,任务的具体工作其实就是在run()方法里执行。
当一个任务通过execute(Runnable)方法欲添加到线程池时,根据你线程池和所选择的拒绝策略对任务进行相应的处理,具体如下:
1、线程池中的任务数量小于corePoolSize,则任务直接被执行
2、线程池中的任务数量大于等于corePoolSize,同时workQueue未满,则将任务添加到缓冲队列workQueue中。
3、线程池中任务数量大于等于corePoolSize,workQueue同时满,但是线程池中运行的任务数目小于maxinumPoolSize时,则重新开辟线程执行新任务。
4、线程池中的任务数量大于等于corePoolSize,workQueue也满了,同时maxinumPoolSize也超了,那么新任务将被拒绝,并根据相应的拒绝策略来进行处理。相应的拒绝策略处理在上面有所介绍。
介绍完这些以后,下面我们看一个列子:
import
java.io.Serializable;
import
java.util.concurrent.ArrayBlockingQueue;
import
java.util.concurrent.ThreadPoolExecutor;
import
java.util.concurrent.TimeUnit;
public
class
TestThreadPool2
{
private
static
int
produceTaskSleepTime =
3
;
private
static
int
produceTaskMaxNumber =
10
;
public
static
void
main(String[] args)
{
// 构造一个线程池
ThreadPoolExecutor threadPool =
new
ThreadPoolExecutor(
2
,
4
,
5
, TimeUnit.SECONDS,
new
ArrayBlockingQueue<Runnable>(
3
),
new
ThreadPoolExecutor.CallerRunsPolicy());
for
(
int
i =
1
; i <= produceTaskMaxNumber; i++)
{
try
{
// 产生一个任务,并将其加入到线程池
String task =
"task@ "
+ i;
System.out.println(
"put "
+ task);
threadPool.execute(
new
ThreadPoolTask(task));
// 便于观察,等待一段时间
// Thread.sleep(produceTaskSleepTime);
}
catch
(Exception e)
{
e.printStackTrace();
}
}
}
}
/**
* 线程池执行的任务
*/
class
ThreadPoolTask
implements
Runnable, Serializable
{
private
static
final
long
serialVersionUID =
0
;
private
static
int
consumeTaskSleepTime =
2000
;
// 保存任务所需要的数据
private
Object threadPoolTaskData;
ThreadPoolTask(Object tasks)
{
this
.threadPoolTaskData = tasks;
}
public
void
run()
{
// 处理一个任务,这里的处理方式太简单了,仅仅是一个打印语句
System.out.println(
"当前运行的线程是:"
+threadPoolTaskData);
System.out.println(
"start .."
+ threadPoolTaskData);
try
{
// //便于观察,等待一段时间
Thread.sleep(consumeTaskSleepTime);
}
catch
(Exception e)
{
e.printStackTrace();
}
System.out.println(
"线程:"
+threadPoolTaskData+
"运行结束"
);
threadPoolTaskData =
null
;
// System.out.println("线程:"+threadPoolTaskData+"运行结束");
}
public
Object getTask()
{
return
this
.threadPoolTaskData;
}
}
思考:
1、当新任务到达时,execute(Runnable)处理方式有四种情况,其中说到的第三种情况,是否会导致后来的任务先运行的情况。
这里肯定会出现这种情况的,即第三种情况中,后来的任务会先于缓冲队列中的任务执行。通常我们都会让任务有个先来后,那么如何避免出现这种情况呢,其实我们可以设置corePoolSize=maxinumPoolSize。
2、关于缓冲队列的思考。
通过查阅jdk文档知道BlockingQueue是个接口,支持两个附加操作的 Queue,这两个操作是:获取元素时等待队列变为非空,以及存储元素时等待空间变得可用。 那么它的具体实现有哪些呢?
(1)ArrayBlockingQueue,一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。个人不建议线程池中使用这种队列,因为线程池中涉及到频繁的删除插入操作(即出队入队操作),这样难免涉及到元素的移动,而这是一个消耗资源的操作。
(2)LinkedBlockingQueue,一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。在线程池中本人侧重喜欢使用这种队列。
(3)SynchronousQueue,一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。也即队列中只有存在一个元素。
(4)DelayQueue,该队列是一个延迟队列,即队列中的每一个元素只有延迟期满之后才能被take出来。
(5)LinkedBlockingDeque,双向并发阻塞队列。所谓双向是指可以从队列的头和尾同时操作,并发只是线程安全的实现,阻塞允许在入队出队不满足条件时挂起线程。每一个结点有前后两个引用,这样才能将所有元素串联起来,支持双向遍历。
(6) PriorityBlockingQueue,要了解PriorityBlockingQueue我们首先了解PriorityQueue,那么PriorityQueue是什么队列呢?它是一个基于优先级堆的无界优先级队列。优先级队列的元素按照其自然顺序进行排序,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。优先级队列不允许使用 null 元素。依靠自然顺序的优先级队列还不允许插入不可比较的对象(这样做可能导致 ClassCastException)。 此队列的头 是按指定排序方式确定的最小 元素。如果多个元素都是最小值,则头是其中一个元素——选择方法是任意的。队列获取操作 poll、remove、peek 和 element 访问处于队列头的元素。而PriorityBlockingQueue一个无界阻塞队列,它使用与类 PriorityQueue 相同的顺序规则,并且提供了阻塞获取操作。