目录
本篇主要讲解的是jdk自带的线程池,具体线程池的作用可看链接。先来看看线程池的类结构图:
![](https://i-blog.csdnimg.cn/blog_migrate/31bc7cdd784ea7d7e60e81323e24bb59.png)
本篇关键是掌握ThreadPoolExecutor和ScheduledThreadPoolExecutor和ForkJoinPool三个线程池实现类,熟练掌握这三个线程池可以说已经很够用了。
ThreadPoolExecutor
线程池的状态
ThreadPoolExecutor使用int的高3位来表示线程池状态,低29位表示线程数量
状态名 | 高三位 | 接受新任务 | 处理阻塞队列任务 | 说明 |
---|---|---|---|---|
RUNNING | 111 | Y | Y | 正常状态 |
SHUTDOWN | 000 | N | Y | 不会接收新任务但会处理阻塞队列剩余任务 |
STOP | 001 | N | N | 会立即中断正在执行的任务,并抛弃阻塞队列里的任务 |
TIDYING | 010 | - | - | 任务全执行完毕,活动线程为0即将进入TIDYING状态,等同于一个过渡状态,即将进入终结状态 |
TERMINATED | 011 | - | - | 终结状态 |
这些信息存储在一个原子变量ctl中,目的是将线程池状态与线程个数合二为一,这样就可以用一次cas 原子操作进行赋值,节省时间。
关于状态信息,可以看它的源码,如下:其中的注释已经写得很明了了,具体的状态装换也说明了
/**
* 状态信息
* RUNNING: Accept new tasks and process queued tasks
* SHUTDOWN: Don't accept new tasks, but process queued tasks
* STOP: Don't accept new tasks, don't process queued tasks,
* and interrupt in-progress tasks
* TIDYING: All tasks have terminated, workerCount is zero,
* the thread transitioning to state TIDYING
* will run the terminated() hook method
* TERMINATED: terminated() has completed
状态转换方法
* RUNNING -> SHUTDOWN
* On invocation of shutdown(), perhaps implicitly in finalize()
* (RUNNING or SHUTDOWN) -> STOP
* On invocation of shutdownNow()
* SHUTDOWN -> TIDYING
* When both queue and pool are empty
* STOP -> TIDYING
* When pool is empty
* TIDYING -> TERMINATED
* When the terminated() hook method has completed
*
* Threads waiting in awaitTermination() will return when the
* state reaches TERMINATED.
*/
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;//29
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
//合二为一,rs高三位为线程池状态,wc低29位是线程个数
private static int ctlOf(int rs, int wc) { return rs | wc; }
![](https://i-blog.csdnimg.cn/blog_migrate/4303646b9e82832286340eb451b51b8a.png)
构造方法
ThreadPoolExecutor的构造方法十分的重要,其中最全的构造方法源码如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
- corePoolSize:核心线程数目,懒惰创建的
- maximumPoolSize:最大线程数目=核心线程数+救急线程数,如果核心线程数指定为3最大线程数为5,那么最多还可以创建出2个救急线程
- keepAliveTime:救急线程的生存时间
- unit:时间单位,针对救急线程
- workQueue:BlockingQueue阻塞队列,用于存放任务队列、阻塞线程池中的线程、获取和提交任务到任务队列,一般使用LinkedBlockingQueue(FIFO)实现类即可,它的构造方法可以指定任务队列的最大容量(默认Integer.MAX_VALUE为任务队列的最大容量)
- threadFactory:线程工厂接口,有一个newThread方法可以由我们自己实现,线程池中创建新线程时就是依靠该工厂的Thread newThread(Runnable r)方法,一般用于diy线程池中的线程名
- handler:拒绝策略,当阻塞队列的任务队列装不下其他任务了,触发拒绝策略,决定这些任务何去何从的策略,jdk提供了四种拒绝策略
其中的救急线程比较特殊,即当前任务突发量比较大,阻塞队列也放不下这么多任务了,此时不会立即执行拒绝策略,首先会查看现在还能否创建救急线程(maximumPoolSize-corePoolSize>0?),如果可以的话,就创建救急线程,去执行那些放不下的任务,直到救急线程全部在忙碌还是有任务装不进任务队列,那么就会触发拒绝策略。
救急线程和核心线程的区别就是:救急线程可以指定生存时间,如果超过了指定的时间没有得到新任务就会销毁,核心线程创建出来了会一直在线程池中
工作流程
- 线程池中刚开始没有线程(懒加载),当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
- 当线程数达到corePoolSize,这时再加入任务,新加的任务会被加入BlockingQueue(workQueue)队列排队,直到有空闲的线程。
- 如果队列选择了有界队列,那么任务超过了任务队列的大小时,会创建maximumPoolSize - corePoolSize数目的线程来救急。
- 如果线程到达maximumPoolSize仍然有新任务这时会执行拒绝策略。拒绝策略jdk提供了4种实现其它著名框架也提供了实现
-
AbortPolicy 让调用者抛出RejectedExecutionException异常,这是默认策略
-
CallerRunsPolicy 让调用者运行任务
-
DiscardPolicy放弃本次任务
-
DiscardOldestPolicy放弃队列中最早的任务,本任务取而代之
-
Dubbo的实现,在抛出RejectedExecutionException异常之前会记录日志,并dump线程栈信息,方便定位问题
-
Netty的实现,是创建一个新线程来执行任务
-
-
当高峰过去后,超过corePoolSize的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime和unit来控制。
工厂方法
根据上面的ThreadPoolExecutor的构造方法,为了简单易用,JDK的Executors类封装了这些构造方法,提供了很多工厂方法,用来创建各种用途的线程池。
1.newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
只需要指定一个int类型的线程数,核心线程数和最大线程数都为该固定值,所以也就没有救急线程,因此也无需超时时间,额外的有也可以指定线程工厂类。
该构造出来的线程池适用于任务数已知,相对耗时的任务
演示:
ExecutorService threadPool = Executors.newFixedThreadPool(2, new ThreadFactory() {
AtomicInteger count = new AtomicInteger(0);//计数器
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "My-Worker" + count.incrementAndGet());
}
});
threadPool.submit(()-> log.debug("{}",1));
threadPool.submit(()-> log.debug("{}",2));
threadPool.submit(()-> log.debug("{}",3));
//15:09:15.087 [My-Worker1] 1
//15:09:15.087 [My-Worker2] 2
//15:09:15.090 [My-Worker1] 3
2.newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//还有一个就是带线程工厂的构造,同上
该构造方法创建出来的线程池核心线程为0,最大线程为Integer.MAX_VALUE,急救线程的生存时间为60秒,意味着全部是救急线程。
队列采用了SynchronousQueue实现特点是,它没有容量,没有线程来取任务是放不进去的,也就是没存储任务的空间,放任务的线程必须阻塞等待救急线程去取该任务。因为都是救急线程且救急线程能够创建很多个,所以放任务的线程一般不会等很久
整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲1分钟后释放线程。
适合任务数比较密集,但每个任务执行时间较短的情况。
3.newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
只有一个线程,没有救急线程,当任务数多于1时,会放入队列。相当与开了一个线程去串行的执行任务,但是自己开的线程和线程池的单线程还是有区别的
特点:
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施;而线程池还会新建一个线程,保证池的正常工作
- Executors.newSingleThreadExecutor()线程个数始终为1,不能修改
- FinalizableDelegatedExecutorService应用的是装饰器模式,只对外暴露了ExecutorService接口,因此不能调用ThreadPoolExecutor中特有的方法
- Executors.newFixedThreadPool(1)初始时为1,以后还可以修改
- 对外暴露的是ThreadPoolExecutor对象,可以强转后调用setCorePoolSize等方法进行修改
如下代码,可以看出第一个任务异常后,相应的线程结束后,线程池又重新起了一个线程
ExecutorService threadPool = Executors.newSingleThreadExecutor();
threadPool.execute(() -> {
log.debug("1");
int a = 1 / 0;
});
threadPool.execute(() -> log.debug("2"));
threadPool.execute(() -> log.debug("3"));
//15:35:19.292 [pool-2-thread-1] 1
//15:35:19.295 [pool-2-thread-2] 2
//15:35:19.295 [pool-2-thread-2] 3
//Exception in thread "pool-2-thread-1" java.lang.ArithmeticException: / by zero
提交方法
//提交任务,只能接收Runnable的任务,且没有返回值和异常信息
void execute(Runnable command);
//提交任务Callable,用返回值Future获取任务执行结果和异常信息
<T> Future<T> submit(Callable<T> task);
//提交tasks中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)throws InterruptedException;
//提交tasks中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
throws InterruptedException;
//提交所有任务,哪个任务先执行完毕,返回此任务的结果,其他则任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)throws InterruptedException, ExecutionException;
//带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
关闭线程池
![](https://i-blog.csdnimg.cn/blog_migrate/4303646b9e82832286340eb451b51b8a.png)
1.shutdown
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//修改线程池状态
advanceRunState(SHUTDOWN);
//打断空闲的线程,不会打断正在执行任务的线程
interruptIdleWorkers();
onShutdown();
} finally {
mainLock.unlock();
}
//尝试终结,如果运行线程数为0的话就可以终结线程池了
tryTerminate();
}
演示:
public static void main(String[] args) {
//需要强转一下,否则的话一些ThreadPoolExecutor自己扩展的功能用不了
ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
Future<String> submit = threadPool.submit(() -> {
log.debug("task1 running...");
Thread.sleep(1000);
log.debug("task1 end...");
return "1";
});
Future<String> submit2 = threadPool.submit(() -> {
log.debug("task2 running...");
Thread.sleep(1000);
log.debug("task2 end...");
return "2";
});
Future<String> submit3 = threadPool.submit(() -> {
log.debug("task3 running...");
Thread.sleep(1000);
log.debug("task3 end...");
return "3";
});
log.debug("shutdown");
threadPool.shutdown();
//17:04:05.732[main] - 197 shutdown
//17:04:05.732[pool - 2 - thread - 1] - 197 task1 running...
//17:04:05.732[pool - 2 - thread - 2] - 197 task2 running...
//17:04:06.745[pool - 2 - thread - 2] - 1210 task2 end...
//17:04:06.745[pool - 2 - thread - 1] - 1210 task1 end...
//17:04:06.745[pool - 2 - thread - 2] - 1210 task3 running...
//17:04:07.757[pool - 2 - thread - 2] - 2222 task3 end...
}
如上,当shutdown后,任务队列的线程依然会被执行完,只不过shutdown后再提交任务就会抛异常。执行shutdown的线程并不会阻塞等待任务完成。
2.shutdownNow
//会将队列中的剩余任务进行返回
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//修改状态
advanceRunState(STOP);
//打断所有线程,包括正在执行的
interruptWorkers();
//获取阻塞队列中的剩余任务
tasks = drainQueue();
} finally {
mainLock.unlock();
}
//尝试终结
tryTerminate();
return tasks;
}
同样的代码,shutdownNow会立即停止,并且打断正在执行任务的线程
log.debug("shutdownNow");
//任务列表也响应的返回了
List<Runnable> runnables = threadPool.shutdownNow();
log.debug("{} ", runnables);
//17:08:02.378 [main] - 220 shutdown
//17:08:02.378 [pool-2-thread-1] - 220 task1 running...
//17:08:02.378 [pool-2-thread-2] - 220 task2 running...
//17:08:02.378 [pool-2-thread-2] - 221 [java.util.concurrent.FutureTask@357246de]
3.awaitTermination
如果想让调用shutdown方法的线程阻塞等待所有任务完成,可以在shutdown方法后面加上
threadPool.awaitTermination(3,TimeUnit.SECONDS);
该方法的参数是最多阻塞等待多少时间。
饥饿
固定大小线程池会有饥饿现象
看如下一段程序:
public static void main(String[] args) throws InterruptedException {
//只有两个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
threadPool.execute(() -> {
log.debug("洗茶壶...");//先洗好茶壶,再把烧水任务提交到线程池
Future<String> future = threadPool.submit(() -> {
Thread.sleep(2000);
log.debug("水烧开了");
return "水";
});
try {
//等待水烧开,再喝茶
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
log.debug("喝茶...");
});
//同样的动作
threadPool.execute(() -> {
log.debug("洗茶壶...");
Future<String> future = threadPool.submit(() -> {
Thread.sleep(2000);
log.debug("水烧开了");
return "水";
});
try {
String s = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
log.debug("喝茶...");
});
}
该程序的结果如下
17:36:13.753 [pool-2-thread-1] lxc.thread.Demo - 207 洗茶壶...
17:36:13.753 [pool-2-thread-2] lxc.thread.Demo - 207 洗茶壶...
两个任务都是洗好水壶了,都要阻塞等待烧完水才执行结束,此时两个线程都占有这2个唯一的工作线程,所以烧水的任务谁也得不到运行,导致发生了线程池的饥饿现象。
解决方法:可以单纯的增加线程数,但是更好的解决方法是,不同任务类型应该使用不同的线程池,如洗茶壶用一个特定的线程池,烧水用一个特定的线程池,这样能够避免饥饿,并能提升效率。
创建多少线程合适
- 过小会导致程序不能充分地利用系统资源、容易导致饥饿
- 过大会导致更多的线程上下文切换,占用更多内存
CPU密集型运算
通常采用cpu核数+1能够实现最优的CPU利用率,+1是保证当线程由于页缺失(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证CPU时钟周期不被浪费
I/O密集型运算
CPU不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用CPU资源,但当你执行IO操作时、远程RPC调用时,包括进行数据库操作时,这时候CPU就闲下来了,你可以利用多线程提高它的利用率。
线程数=核数×期望cpu利用率 ×总时间(CPU计算时间+等待时间)/ CPU计算时间
例如4核CPU计算时间是50%,其它等待时间是50%,期望cpu被100%利用,套用公式
4×100%×100% / 50% = 8
例如4核CPU计算时间是10%,其它等待时间是90%,期望cpu被100%利用,套用公式
4×100%×100% / 10% = 40
线程池的监控
对线程池进行监控是很有必要的,方便在出现问题进行排查,在监控线程池时可以根据使用状况快速定位问题。在监控线程池时可以使用到如下属性:
- taskCount:线程池需要执行的总任务数量
- completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount
- largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个可以知道线程池曾经是否满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
- getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池中的线程是不会自动销毁的,所以这个大小只增不减
- getActiveCount:获取活动的线程数
通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,这几个方法在线程池中是空方法。代码如下:
@Slf4j
public class MyThreadPoolExecutor extends ThreadPoolExecutor {
public MyThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override//任务执行前执行
protected void beforeExecute(Thread t, Runnable r) {
log.debug("taskCount:{}",getTaskCount());
log.debug("completedTaskCount:{}",getCompletedTaskCount());
log.debug("largerPoolSize:{}",getLargestPoolSize());
log.debug("poolSize:{}",getPoolSize());
log.debug("activeCount:{}",getActiveCount());
log.debug("before.........");
}
@Override//任务执行完后执行
protected void afterExecute(Runnable r, Throwable t) {
log.debug("after........");
}
@Override //线程池终止前执行
protected void terminated() {
log.debug("terminated........");
}
}
ScheduledExecutorService
该类的子实现类为ScheduledThreadPoolExecutor,比上面的ThreadPoolExecutor线程池多了一个任务调度的功能,可以延迟执行任务或者是定时执行任务,但是该线程池不会创建救急线程,因为它的构造方法调用了父类ThreadPoolExecutor的构造方法,且置救急线程的存活时间为0。
ThreadPoolExecutor代码如下:
public interface ScheduledExecutorService extends ExecutorService {
//延迟执行Runnable,延迟的时间为delay
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
//延迟执行Callable
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay, TimeUnit unit);
//定时执行,initialDelay为初始延迟,period为每次执行的间隔时间
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
//定时执行,和scheduleAtFixedRate有区别
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
延迟执行任务
延迟执行也没别的了,就是把一个任务放进任务队列,然后延迟时间一到就会有线程来执行该任务,只执行一次。
如下代码是一个比较特殊的延迟任务,线程池中只有一个线程,且第一个任务执行要的时间比较久,可以看到第二个任务本来是3秒后执行的,但是因为只有一个worker(),在2秒后该worker去执行任务一了,且执行了3秒,所以没有worker去执行3秒后执行的任务二,这也导致了事件上的错误,这是一个需要注意的点。
public static void main(String[] args){
//只有一个线程的线程池
ScheduledThreadPoolExecutor threadPool = new ScheduledThreadPoolExecutor(1);
threadPool.schedule(() -> {
log.debug("1");
sleep(3000);
}, 2, TimeUnit.SECONDS);
threadPool.schedule(() -> {
log.debug("2");
}, 3, TimeUnit.SECONDS);
log.debug("main");
//18:54:45.529 [main] main
//18:54:47.530 [pool-2-thread-1] 1
//18:54:50.537 [pool-2-thread-1] 2
}
定时执行任务
在一定延迟后,每隔一定的时间都去执行一个任务就是定时执行任务,演示如下:
ScheduledThreadPoolExecutor threadPool = new ScheduledThreadPoolExecutor(1);
threadPool.scheduleWithFixedDelay(()->{
log.debug("1");
},2,1,TimeUnit.SECONDS);
log.debug("main");
//19:13:30.280 [main] main
//19:13:32.282 [pool-2-thread-1] 1
//19:13:33.296 [pool-2-thread-1] 1
//19:13:34.311 [pool-2-thread-1] 1
//19:13:35.324 [pool-2-thread-1] 1
scheduleWithFixedDelay和scheduleAtFixedRate都能实现,区别是:如果一个定时任务执行所耗费的时间非常长,那么中间是with的方法就是不管你执行要多少时间,我只管本次任务执行完了才开始计时,到一定时间了再执行下一次;而中间是at的方法是任务一开始执行就开始计时。
如下测试scheduleWithFixedDelay:40秒的时候开始提交了任务,然后延迟一秒,41秒开始执行第一次,执行完需要耗费3秒,44秒执行完,然后开始计时,过了1秒后到45秒开始执行第二次任务
public static void main(String[] args) throws InterruptedException {
ScheduledThreadPoolExecutor threadPool = new ScheduledThreadPoolExecutor(1);
threadPool.scheduleWithFixedDelay(() -> {
log.debug("1");
sleep(3000);//一个任务需要3秒才能执行完
}, 1, 1, TimeUnit.SECONDS);
log.debug("main");
//19:23:40.405 [main] main
//19:23:41.419 [pool-2-thread-1] 1
//19:23:45.427 [pool-2-thread-1] 1
//19:23:49.442 [pool-2-thread-1] 1
//19:23:53.450 [pool-2-thread-1] 1
}
如下测试scheduleAtFixedRate:15开始提交了任务,16开始第一次任务的执行,注意此时开始计时第二次执行的时间应该是第18秒,然后第一次任务执行了3秒后到了第19秒,立即马上执行了第二个任务,因为第一个的执行时间太长了,导致第二次本来是第18秒应该开始执行的被拖到了第19秒执行,可以理解吗?
public static void main(String[] args) throws InterruptedException {
ScheduledThreadPoolExecutor threadPool = new ScheduledThreadPoolExecutor(2);
threadPool.scheduleAtFixedRate(() -> {
log.debug("1");
sleep(3000);//一个任务需要3秒才能执行完
}, 1, 1, TimeUnit.SECONDS);
log.debug("main");
//19:27:15.936 [main] main
//19:27:16.946 [pool-2-thread-1] 1
//19:27:19.949 [pool-2-thread-1] 1
//19:27:22.961 [pool-2-thread-1] 1
//19:27:25.969 [pool-2-thread-1] 1
}
处理异常
线程池在执行任务的时候,即使出现了异常也不会抛出异常信息,需要我们手动去捕捉。
如下,并不会打印异常信息
pool.schedule(()->{
log.debug("执行");
int i=1/0;
},1,TimeUnit.SECONDS);
-
由任务自己trycatch捕捉异常
-
使用Future获取返回值,执行任务用Callable得到异常
定时任务应用
//每周四 18:00:00执行一个定时任务
public static void main(String[] args) throws InterruptedException {
//得到当前时间
LocalDateTime now = LocalDateTime.now();
log.debug("{}", now);
//找到本周四的时间
LocalDateTime target = now.withHour(18).withMinute(0).withSecond(0).with(DayOfWeek.THURSDAY);
//如果当前时间>本周周四,则找到下一周
if (now.compareTo(target) > 0) {
target = target.plusWeeks(1);
}
System.out.println(target);
//相隔的时间差
long initialDelay = Duration.between(now, target).toMillis();
long period = 1000 * 60 * 60 * 24 * 7;
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(1);
pool.scheduleAtFixedRate(() -> {
log.debug("hello");
}, initialDelay, period, TimeUnit.MILLISECONDS);
}
Fork/Join
Fork/Join是JDK 1.7加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的cpu密集型运算。
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
Fork/Join默认会创建与cpu核心数大小相同的线程池
解决任务步骤
步骤一:分割任务,把大任务分成若干个互不依赖的子任务,这也是一个难点
步骤二:最后再把子任务的结果进行汇总
ForkJoin使用两个类来完成以上两种事情。
①ForkJoinTask:使用Fork/Join框架必须首先创建ForkJoinTask任务,它提供了在任务中执行fork()和join()的机制,fork()是拆分任务给线程池中的其他线程执行,join()不是Thread类的join,它用于等待子任务执行完成,获取结果,最后由我们自己汇总结果。我们不需要直接继承ForkJoinTask,只需继承它的两个子类:
- RecursiveAction:用于没有返回结果的任务
- RecursiveTask:用于有返回结果的任务,它的泛型就是返回结果的类型
②ForkJoinPool:ForkJoinTask需要通过ForkJoinPool中的线程来执行
ForkJoin原理是采用的工作窃取算法:线程池中的每个线程都会把分割出来的子任务添加到自己维护的双端队列中,任务进入队列的头部。当一个线程的队列里没有任务时,会随机的从其他线程的队列中窃取一个任务(从队列尾部窃取)来执行。
应用
计算一个0+1+2+3+……+100的结果,此+非彼+,这里的加法是我自定义的一个运算方法,该运算每次都需要执行200ms,我们采用ForkJoin并行计算,把这100次计算变为10次小的计算,每次计算10个数,如线程1计算0~9,线程2计算10~19……,代码如下:
@Slf4j
public class ForkJoinDemo {
public static void main(String[] args) throws InterruptedException {
//构造ForkJoin线程池,可以指定线程数
ForkJoinPool pool = new ForkJoinPool();
long start = System.currentTimeMillis();
//我们的任务 0到100的计算
MyTask task = new MyTask(0, 100);
//使用线程池执行该任务
Integer invoke = pool.invoke(task);
long end = System.currentTimeMillis();
log.debug("ForkJoin并行计算花费了{}ms,结果为{}", end - start, invoke);
//串行计算,时间对比
invoke = 0;
start = System.currentTimeMillis();
for (int i = 0; i <= 100; i++) {
invoke = mySum(invoke, i);
}
end = System.currentTimeMillis();
log.debug("串行计算花费了{}ms,结果为{}", end - start, invoke);
//11:27:48.554 [ForkJoinPool-1-worker-5] 40-49计算的结果为:445
//11:27:48.554 [ForkJoinPool-1-worker-0] 70-79计算的结果为:745
//11:27:48.554 [ForkJoinPool-1-worker-7] 60-69计算的结果为:645
//11:27:48.554 [ForkJoinPool-1-worker-3] 20-29计算的结果为:245
//11:27:48.554 [ForkJoinPool-1-worker-1] 0-9计算的结果为:45
//11:27:48.554 [ForkJoinPool-1-worker-2] 10-19计算的结果为:145
//11:27:48.554 [ForkJoinPool-1-worker-6] 50-59计算的结果为:545
//11:27:48.554 [ForkJoinPool-1-worker-4] 30-39计算的结果为:345
//11:27:50.596 [ForkJoinPool-1-worker-0] 80-89计算的结果为:845
//11:27:50.800 [ForkJoinPool-1-worker-7] 90-100计算的结果为:1045
//11:27:50.800 [main] ForkJoin并行计算花费了4303ms,结果为5050
//11:28:11.324 [main] 串行计算花费了20524ms,结果为5050
}
//自定义的运算方法
public static int mySum(int x, int y) {
try {
Thread.sleep(200);//模拟运算200ms
} catch (InterruptedException e) {
e.printStackTrace();
}
return x + y;
}
@Slf4j
/**
* ForkJoinTask类,返回值为Integer
*/
static class MyTask extends RecursiveTask<Integer> {
private int start, end;
public MyTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override//重写父类的compute方法,写我们任务的具体逻辑
protected Integer compute() {
int res = 0;
//1.如果当前任务计算量已经小于等于10了,就不用再分了,相当于是递归的出口
if (end - start <= 10) {
for (int i = start; i <= end; i++) {
res = mySum(res, i);
}
log.debug("{}-{}计算的结果为:{}", start, end, res);
return res;//直接返回即可,退出递归
}
//2.分割小任务,构建一个新的任务类
MyTask otherTask = new MyTask(start + 10, end);
//把子任务放到自己的双端队列里,其他线程就可以窃取该任务并执行
ForkJoinTask<Integer> fork = otherTask.fork();
//计算本次的任务
for (int i = start; i < start + 10; i++) {
res = mySum(res, i);
}
log.debug("{}-{}计算的结果为:{}", start, start + 10 - 1, res);
//等待子任务的完成,获取结果
Integer join = fork.join();
//汇总结果返回
return res + join;
}
}
}
可以看出并行计算在任务量大的情况下还是很强的。
另外的,任务执行时主类是铺捉不到异常的,可以通过ForkJoinTask的boolean isCompletedAbnormally()方法检测是否正常完成,Throwable getException()获取异常信息。
并行归并排序
来对归并排序做一个并行计算,代码如下
@Slf4j
public class ForkJoinDemo {
//子问题阈值大小
public static final int THRESHOLD = 300;
public static void main(String[] args) {
//随机产生一个数组
int[] random = getArray(12312345);
int[] array = Arrays.copyOf(random, random.length);
MySortTask mySortTask = new MySortTask(array, 0, array.length - 1);
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.invoke(mySortTask);
long end = System.currentTimeMillis();
log.debug("并行排序时间:{}ms", end - start);
//原始归并排序
start = System.currentTimeMillis();
mergerSort(random, 0, random.length - 1);
// Arrays.sort(random);
end = System.currentTimeMillis();
log.debug("串行排序时间:{}ms", end - start);
}
//产生数组大小为num的随机数组
public static int[] getArray(int num) {
int[] array = new int[num];
for (int i = 0; i < num; i++) {
array[i] = (int) (Math.random() * 65535);
}
return array;
}
//原始单线程的归并排序
public static void mergerSort(int[] a, int l, int r) {
if (l == r) return;
int mid = l + (r - l) / 2 + 1;
//先分
mergerSort(a, l, mid - 1);
mergerSort(a, mid, r);
//再治
int[] temp = new int[r - l + 1];
int left = l, right = mid;
for (int i = 0; i < temp.length; i++) {
if ((right > r || a[left] < a[right]) && left < mid)
temp[i] = a[left++];
else temp[i] = a[right++];
}
for (int i = 0; i < temp.length; i++) {
a[l++] = temp[i];
}
}
static class MySortTask extends RecursiveAction {
private int[] array;
private int left, right;
public MySortTask(int[] array, int left, int right) {
this.array = array;
this.left = left;
this.right = right;
}
@Override
protected void compute() {
if (left >= right) {
return;
}
//如果小于阈值,就直接排序,不要再分了,否则会导致效率更低
if (right - left < THRESHOLD) {
Arrays.sort(array,left,right);
return;
}
int mid = (right + left) / 2;
MySortTask leftTask = new MySortTask(array, left, mid);
MySortTask rightTask = new MySortTask(array, mid + 1, right);
leftTask.fork();
rightTask.fork();
//等待左右数组排序完毕
leftTask.join();
rightTask.join();
//合并
int[] temp = new int[right - left + 1];
int l = left, r = mid + 1;
for (int i = 0; i < temp.length; i++) {
if ((r > right || array[l] < array[r]) && l < mid + 1) {
temp[i] = array[l++];
} else {
temp[i] = array[r++];
}
}
for (int value : temp) {
array[left++] = value;
}
}
}
}
结果:
13:04:06.665 [main] 并行排序时间:573ms
13:04:08.347 [main] 串行排序时间:1678ms
可以看到时间是串行的3倍,关键是阈值的设定,如果没有该阈值,速度将变慢很多。
这是因为我们的线程数是有限的,如果没有设定阈值,需要排序的小数组也分裂成两个数组等待其他线程执行,这其中会消耗掉任务调度的时间,这个时间对于排序一个小数组来说是比较耗时的,所以较小的数组直接排序就好了,没有必要再去分裂了。
所以这种并行计算一定要合理的分配任务,这样才能发挥并行计算的最大价值。