前言:今天写了一段让自己头大的代码,没有几十年精神病写不出来的那种。给大家瞅瞅
/**
* 用于向开发库插入数据
* 300万条数据
*/
public class ThreadSaveDB {
public static void main(String[] args) {
int size = 3000000;
for (int i=0;i<size;i++){
ThreadPoolExecutor executor = new ThreadPoolExecutor(30,30,5L, TimeUnit.SECONDS, new SynchronousQueue<>());
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("这里是表插入数据的操作");
}
});
}
}
}
差不多就是这样一段代码了,乍一看还没问题,仔细一看什么鬼。存在的错误有下面几点。
问题一: 我把线程池的创建写在了循环里,导致我每次循环都会创建一次。我一开始还没发现,然后还执行了一边,并且等了差不多15分钟,知道它允许报错抛出内存溢出异常我才发现这个问题。
问题二: 这个是在问题一解决之后发现的,我发现在执行几十次之后程序就停止了。同时还有发现异常:Exception in thread “main” java.util.concurrent.RejectedExecutionException
我发现我并不晓得为什么会出现问题二的情况,并且自己也不是特别了解线程池,那么先去学习了。
查阅视频资料发现创建线程池一般都是通过 JUC包下的Executors工具类来实现的。
ExecutorService executorService = Executors.newFixedThreadPool(10);
ExecutorService executorService1 = Executors.newCachedThreadPool();
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
这是最常用的三种创建线程池的方式,我们点进去看源码。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
好家伙它们三用的不都是同一个对象吗ThreadPoolExecutor
,只是传递的参数不同罢了。
我们分别以不同的线程池实现来执行相同的一段代码来进行对比发现。执行时间由快到慢排序:
- Executors.newCachedThreadPool(); //很快
- Executors.newFixedThreadPool(5); //稍慢
- Executors.newSingleThreadExecutor(); //感觉像是单线程
那么这就说明向ThreadPoolExecutor对象传递不同的参数,会造成不同的影响。
我在下面分别把这三种线程的执行打印出来了,我们配合上面的源码看一下。
- Executors.newSingleThreadExecutor();
我们发现自始至终线程只有一个,看一下源码new ThreadPoolExecutor(1, 1,...)核心线程数,和最大线程数确实是只有一个,那就怪不得了。
- Executors.newCachedThreadPool();
通过执行结果发现他们的线程号都是不一样的,看一下源码new ThreadPoolExecutor(0, Integer.MAX_VALUE,...);核心线程数0,最大线程数Integer的最大值。也就是它每次都会创建一个新的线程去执行,所以它的执行速度很快,但是如果我们要执行几百万次,由于它每次都会创建线程,而线程是CPU调度的基本单位,CPU就直接100%炸掉了。
- Executors.newFixedThreadPool(5);
通过执行结果可以看出线程进行了复用,看一下源码new ThreadPoolExecutor(nThreads, nThreads,...),核心线程数和最大线程数都是我们传入的5。其实他是一次五个线程,执行完成之后归还线程,再执行。这就是线程池最大的特点,线程复用。
我发现之前三种线程池实现中阻塞队列用的都是不同的,那这又有什么区别呢? 赶紧研究起来
让我们来看看BlockingQueue家庭大致有哪些成员
-
ArrayBlockingQueue: 基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
-
DelayQueue: DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
-
LinkedBlockingQueue: 基于链表的阻塞队列,其内部维持着一个数据缓冲队列,当生产者往队列中放入一个数据时,缓存在队列内部;只有当队列缓冲区达到最大值缓存容量时(构造函数指定),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程才会被唤醒。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
如果构造对象时没有指定容量,LinkedBlockingQueue会默认构建一个Integer.MAX_VALUE大小的队列,这样的坏处就是,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
-
PriorityBlockingQueue: 基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
-
SynchronousQueue:相较于前几个队列,SynchronousQueue和它们最大的区别,它是没有缓冲区的。 SynchronousQueue是一种无缓冲的等待队列,消费者直接与生产者进行对接(来一个我消费一个,如果生产者数据没有被消费掉, 那么就等待)。
到这我就知道当时的问题二是怎么产生的了,应该是被阻塞住了。我的线程还没来不及被消费掉,就阻塞在哪里了。
既然都学了这么多了,那干脆把拒绝策略也给学了吧。^_^
四种线程池拒绝策略
-
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。
-
ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常。
-
ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新提交被拒绝的任务
-
ThreadPoolExecutor.CallerRunsPolicy: 由调用线程(提交任务的线程)处理该任务
我们来看下线程池默认的拒绝策略
/**
* The default rejected execution handler
* */
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
嗯嗯丢弃任务并抛出RejectedExecutionException异常,马上写一段代码来测试一下。
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,0L, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(5));
for (int i=0;i<100;i++){
final int index = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("ThreadName:"+Thread.currentThread().getName()+" Index:"+index);
}catch (Exception e){
e.printStackTrace();
}
}
});
}
}
控制台报错了
ThreadName:pool-1-thread-2 Index:1
ThreadName:pool-1-thread-9 Index:13
ThreadName:pool-1-thread-7 Index:11
ThreadName:pool-1-thread-3 Index:2
ThreadName:pool-1-thread-8 Index:12 ......
Exception in thread "main" java.util.concurrent.ReRejectedExecutionException: Task threads.ThreadDemo02$1@266474c2 rejected from java.util.concurrent.ThreadPoolExecutor@6f94fa3e[Running, pool size = 20, active threads = 19, queued tasks = 0, completed tasks = 39]
果然程序抛出了我们预期中的RejectedExecutionException错误。
-- 嗯解读下这段程序
1. 我在这里增加了Thread.sleep(1000);进行了一秒的休眠,是为了方便执行出我们想要的效果,模拟执行业务流程。
2. 我们创建了5个核心线程,10个最大线程数,长度为5的阻塞队列,100次循环。
3. 也就是说他们最多可以同时存下15条数据,10条线程在消费,5条存在队列。但是由于我sleep了1秒,数据仍然在进来,但是又没有线程消费掉没有空位置给它们,这时候拒绝策略就起作用了,丢弃了任务并且抛出了RejectedExecutionException异常。
-- 嗯嗯嗯这就是我的理解了
这是网上找的图,里面的数据可能不一样但是流程是一样的,可以看一下。
拒绝策略场景分析
- AbortPolicy
丢弃任务并抛出RejectedExecutionException异常。
这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。 - DiscardPolicy
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,本人的博客网站统计阅读量就是采用的这种拒绝策略。
和AbortPolicy相似,只是不抛异常了。
- DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务。
此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
上代码
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,0L, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i=0;i<100;i++){
final int index = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("ThreadName:"+Thread.currentThread().getName()+" Index:"+index);
}catch (Exception e){
e.printStackTrace();
}
}
});
}
}
我们来根据它的执行结果来分析流程
-- 15条数据,我把它分为了1、2、3三段。以下是我的理解
1. 段1、段2 是属于核心线程和非核心线程的。总共是10条数据,且是从0开始的。
2. 而段3 我们发现它的Index是最后几位数,那我想它是这样的,1-5,5-10它分别存在了核心线程和非核心线程中,其余的它开始存入阻塞队列,但是由于它的拒绝策略是丢弃队列最前面的任务,然后重新提交被拒绝的任务。那它从10-95 都是被决绝抛弃掉了,然后剩下了最后的95-100.(当然这是多线程不会像我这样描述的数字这么连续,但是流程是不变的)。
- CallerRunsPolicy
由调用线程处理该任务
如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务。
上代码
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,0L, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());
for (int i=0;i<100;i++){
final int index = i;
executor.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println("ThreadName:"+Thread.currentThread().getName()+" Index:"+index);
}catch (Exception e){
e.printStackTrace();
}
}
});
}
}
执行结果
和我们之前的描述一样,如果任务被拒绝了,调用提交任务的线程直接执行此任务。
备注
嗯咨询了下大佬,最好不要用Executors工具类,而是使用ThreadPoolExecutpr,并且根据实际业务场景来配置不同的参数。
最后!我发现了一个很严重的问题 !!
我的线程池是会用了,改对了。
但是为什么我的插表数据这么慢啊,300万条数据 一个小时才插入了40万条,我吐了。
然后被逼无奈我只能去研究如何快速进行批量插入数据了。
导航:https://blog.csdn.net/weixin_45310564/article/details/120562276