阻塞队列与线程池补充
1.LinkedTransferQueue
一个由链表结构组成的无界阻塞队列。
transfer(),必须要消费者消费了以后方法才会返回, tryTransfer()无论消费者是否接收,方法都立即返回。
2.LinkedBlockingDeque
一个由链表结构组成的双向阻塞队列。
可以从队列的头和尾都可以插入和移除元素,实现工作密取,方法名带了First对头部操作,带了last从尾部操作,另外:add=addLast; remove=removeFirst; take=takeFirst
3.自定义线程池
- 1.内部属性
public class MyThreadPool{
// 工作线程组
private WorkThread[] workThreads;
// 任务队列
private final BlockingQueue<Runnable> taskQueue;
// 构造函数
public MyThreadPool(int thread_num, int task_count){
for(int i = 0; i < thread_num; i++){
workThreads[i] = new WorkThread();
workThreads[i].start();
}
}
// 执行任务
public void execute(Runnable task){
taskQueue.put(task);
}
// 销毁线程池
public void destroy(){
for(int i = 0; i < thread_num; i++){
workThread[i].stopWorker();
workThread[i] = null;
}
taskQueue.clear();
}
// 内部类,工作线程
private class WorkThread extends Thread{
@override
public void run(){
Runnable r = null;
try{
while(!isInterrupted){
r = taskQueue.take();
if(r != null){
r.run();
}
}
}
}
}
public void stopWorker(){
interrupt();
}
}
4.合理配置线程池
根据任务的性质:计算密集型(CPU),IO密集型
计算密集型:加密、大数分解、正则:
推荐:CPU核心数:Runtime.getRuntime().availableProcessors();
IO密集型:读取文件,数据库连接,网络通讯…
推荐:CPU核心数 * 2
5.ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor是一个使用线程池执行定时任务的类,相较于Java中提供的另一个执行定时任务的类Timer,其主要有如下两个优点:
- 使用多线程执行任务,不用担心任务执行时间过长而导致任务相互阻塞的情况,Timer是单线程执行的,因而会出现这个问题;
- 不用担心任务执行过程中,如果线程失活,其会新建线程执行任务,Timer类的单线程挂掉之后是不会重新创建线程执行后续任务的。
除去上述两个优点外,ScheduledThreadPoolExecutor还提供了非常灵活的API,用于执行任务。其任务的执行策略主要分为两大类:①在一定延迟之后只执行一次某个任务;②在一定延迟之后周期性的执行某个任务。如下是其主要API:
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay, long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay, long period, TimeUnit unit);
上述四个方法中,第一个和第二个方法属于第一类,即在delay指定的延迟之后执行第一个参数所指定的任务,区别在于,第二个方法执行之后会有返回值,而第一个方法执行之后是没有返回值的。第三个和第四个方法则属于第二类,即在第二个参数(initialDelay)指定的时间之后开始周期性的执行任务,执行周期间隔为第三个参数指定的时间,但是这两个方法的区别在于第三个方法执行任务的间隔是固定的,无论上一个任务是否执行完成,而第四个方法的执行时间间隔是不固定的,其会在周期任务的上一个任务执行完成之后才开始计时,并在指定时间间隔之后才开始执行任务。如下是使用scheduleWithFixedDelay()和scheduleAtFixedRate()方法编写的测试用例:
public class ScheduledThreadPoolExecutorTest {
private ScheduledThreadPoolExecutor executor;
private Runnable task;
@Before
public void before() {
executor = initExecutor();
task = initTask();
}
private ScheduledThreadPoolExecutor initExecutor() {
return new ScheduledThreadPoolExecutor(2);;
}
private Runnable initTask() {
long start = System.currentTimeMillis();
return () -> {
print("start task: " + getPeriod(start, System.currentTimeMillis()));
sleep(SECONDS, 10);
print("end task: " + getPeriod(start, System.currentTimeMillis()));
};
}
@Test
public void testFixedTask() {
print("start main thread");
executor.scheduleAtFixedRate(task, 15, 30, SECONDS);
sleep(SECONDS, 120);
print("end main thread");
}
@Test
public void testDelayedTask() {
print("start main thread");
executor.scheduleWithFixedDelay(task, 15, 30, SECONDS);
sleep(SECONDS, 120);
print("end main thread");
}
private void sleep(TimeUnit unit, long time) {
try {
unit.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private int getPeriod(long start, long end) {
return (int)(end - start) / 1000;
}
private void print(String msg) {
System.out.println(msg);
}
}
可以看到,上述两个测试用例代码块基本是一致的,区别在于第一个用例调用的是scheduleAtFixedRate()方法,而第二个用例调用的是scheduleWithFixedDelay()。这里两个用例都是设置的在延迟15s后每个30s执行一次指定的任务,而该任务执行时长为10s。如下分别是这两个测试用例的执行结果:
start main thread
start task: 15
end task: 25
start task: 45
end task: 55
start task: 75
end task: 85
start task: 105
end task: 115
end main thread
start main thread
start task: 15
end task: 25
start task: 45
end task: 55
start task: 75
end task: 85
start task: 105
end task: 115
end main thread
对比上述执行结果可以看出,对于scheduleAtFixedRate()方法,其每次执行任务的开始时间间隔都为固定不变的30s,与任务执行时长无关,而对于scheduleWithFixedDelay()方法,其每次执行任务的开始时间间隔都为上次任务执行时间加上指定的时间间隔。
这里关于ScheduledThreadPoolExecutor的使用有三点需要说明如下:
- ScheduledThreadPoolExecutor继承自ThreadPoolExecutor(ThreadPoolExecutor详解),因而也有继承而来的execute()和submit()方法,但是ScheduledThreadPoolExecutor重写了这两个方法,重写的方式是直接创建两个立即执行并且只执行一次的任务;
- ScheduledThreadPoolExecutor使用ScheduledFutureTask封装每个需要执行的任务,而任务都是放入DelayedWorkQueue队列中的,该队列是一个使用数组实现的优先队列,在调用ScheduledFutureTask::cancel()方法时,其会根据removeOnCancel变量的设置来确认是否需要将当前任务真正的从队列中移除,而不只是标识其为已删除状态;
- ScheduledThreadPoolExecutor提供了一个钩子方法decorateTask(Runnable, RunnableScheduledFuture)用于对执行的任务进行装饰,该方法第一个参数是调用方传入的任务实例,第二个参数则是使用ScheduledFutureTask对用户传入任务实例进行封装之后的实例。这里需要注意的是,在ScheduledFutureTask对象中有一个heapIndex变量,该变量用于记录当前实例处于队列数组中的下标位置,该变量可以将诸如contains(),remove()等方法的时间复杂度从O(N)降低到O(logN),因而效率提升是比较高的,但是如果这里用户重写decorateTask()方法封装了队列中的任务实例,那么heapIndex的优化就不存在了,因而这里强烈建议是尽量不要重写该方法,或者重写时也还是复用ScheduledFutureTask类。
6.ComletionService
如果向Executor提交了一组计算任务,并且希望在计算完成后获得结果,那么可以保留每个任务关联的Future,通过轮询来判断任务是否完成。
例如:
public void testByQueue() throws Exception {
ThreadPoolExecutor executors = new ThreadPoolExecutor(cpu, cpu, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000));
long start = System.currentTimeMillis();
for (int i = 0; i < TASK_COUNT; i++) {
Future<Integer> future = executors.submit(new WorkTask());
queue.put(future);
}
for (int i = 0; i < TASK_COUNT; i++) {
Integer sleptTime = queue.take().get();
System.out.println("sleptTime=" + sleptTime + "ms");
ato.getAndAdd(sleptTime);
}
executors.shutdown();
System.out.println("-------------tasks sleep time " + ato.get() + "ms,and spend time "
+ (System.currentTimeMillis() - start) + " ms");
}
带来的问题:i = 0 先取到;i = 1 后取到;不管他们谁先完成。
通过CompletionSerivce来实现获取线程池中任务的结果:
public void testByCompletionService() throws Exception {
ThreadPoolExecutor executors = new ThreadPoolExecutor(cpu, cpu, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000));
ExecutorCompletionService<Integer> cs = new ExecutorCompletionService<>(executors);
long start = System.currentTimeMillis();
for (int i = 0; i < TASK_COUNT; i++) {
cs.submit(new WorkTask());
}
for (int i = 0; i < TASK_COUNT; i++) {
Integer sleptTime = cs.take().get();
System.out.println("sleptTime=" + sleptTime + "ms");
ato.getAndAdd(sleptTime);
}
executors.shutdown();
System.out.println("-------------tasks sleep time " + ato.get() + "ms,and spend time "
+ (System.currentTimeMillis() - start) + " ms");
}
谁先执行完成,结果就最先拿到