java多线程关于ThreadPool
一、自定义ThreadFactory和线程池的工厂类Executors
自定义ThreadFactory,可以自定义线程的名字
public class MyThreadFactory implements ThreadFactory {
private final String prefix = "async-task-";
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(null,r,prefix + threadNumber.getAndIncrement());
return thread;
}
}
Executors可以用来创建线程池,但是不太推荐。
可以使用ThreadPoolExecutor来自定义线程池,来适用当前业务
Executors.newCachedThreadPool:
创建的是无界线程池(可以创建的线程数量是Integer.MAX_VALUE),缺点:高并发下内存占用率大幅升高,导致内存溢出或者系统性能严重下降。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Executors.newFixedThreadPool:
创建时需要传入核心线程池的线程数,并使用LinkedBlockingQueue作为排队队列,创建的线程数是有限的。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Executors.newSingleThreadExecutor
核心线程只有一个,其它提交的线程全部放入队列等待执行。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
二、ThreadPoolExecutor类的参数和使用
2.1、参数介绍:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
KeepaliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2),
new MyThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
corePoolSize: 线程池中至少要保留的线程,核心线程默认是不会超时销毁的。pool.allowCoreThreadTimeOut(true),可以开启核心线程超时销毁。
maximumPoolSize:可创建的最大线程数。
KeepaliveTime:当线程数量大于corePoolSize,没有超过指定的时间是不能从线程池中将线程删除的。范围是maximumPoolSize~corePoolSize之间的线程。是核心线程之外的线程
unit:KeepaliveTime参数的时间单位
线程队列:
1、使用linkedBlockingQueue做队列,linkedBlockingQueue是有界的,默认Integer.MAX_VALUE
2、ArrayBlockingQueue队列。实例化时必须传入初始容量,并且容量不可扩充,超出初始容量会报错。
3、SynchronousQueue队列,该队列不存储数据,通过该队列,可以在两个线程之间传递数据
WorkQueue:执行任务的任务队列,保存有executor提交的任务
2.2、线程池的队列类型
使用 LinkedBlockingQueue<>()无界队列的情况
1、对于存在corePoolSIze和无参new LinkedBlockingQueue<>()队列的情况下,无论提交了多少个任务,线程池中的线程数都不会超过corePoolSize。
2、对于提交的线程数大于corePoolSIze的情况,只会创建corePoolSIze个线程,其他的任务全部放入队列中的等待执行。
使用 LinkedBlockingQueue<>(n)有界队列的情况
1、对于提交的线程数大于maximumPoolSize + n 的情况,线程池中会立即创建并不多于maximumPoolSize个线程来执行任务,多于maximumPoolSize+ n的额外任务会被拒绝并抛出异常。
使用SynchronousQueue<>()的情况
1、如果提交的线程数量大于corePoolSize,而小于等于maximumPoolSize,则创建最多不超过maximumPoolSize个线程来执行任务,如果空闲线程存活时间超过keepaliveTime,则超过corePoolSize之外的线程会被杀死,其他线程依然存活。
2、如果提交任务数量大于maximumPoolSize,则抛出异常。
2.3、线程池的拒绝策略
1、AbortPolicy
当任务添加到线程池被拒绝时,直接抛出RejectedExceutionException异常,是线程池的默认策略。
2、CallerRunsPolicy
被拒绝后,会被加入到调用线程池的线程去处理,可能会造成主线程的阻塞。
比较好的处理方法是在主线程中创建一个线程,并由该线程负责线程池的调用。
3、DiscardOldestPolicy
当任务添加线程池被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列。
4、Discardpolicy
当任务添加到线程池中被拒绝时,线程池会丢弃被拒绝的任务。
三、ThreadPoolExecutor 其他的一些方法
afterExecute()和beforeExecute()
重写这两个方法可以对线程池的线程对象实现监控。
remove()方法
对于execute 提交的任务,remove方法可以删除未运行的thread,但是对于正在运行的thread无法删除。
对于submit 提交的任务,remove方法不能删除。
execute方法: 返回void,只能提交Runnable类型任务
submit方法: 可以提交Runnable和Callable类型的任务,当提交Runnable时返回值是null,提交的类型是callable时,返回的类型是Future。
submit和submit方法对于异常处理的区别:
submit方法是是捕获了异常的,当调用FutureTask的get方法时,才会抛出异常。运行时出现异常不会抛出。
但是execute方法,在运行时有异常就会抛出。
四、工作队列、线程池大小的选择
工作队列
工作队列可以使用有界队列,也可以使用无界队列,或者直接交接队列(SynchronousQueue)。
对于无界队列,虽然无界队列本身不限制线程池任务数,但是无界队列的容量还是取决于任务本身对资源的使用情况。如果创建任务时引用了其他的对象,而这些对象又特别大,随着队列中任务越多,会导致这些对象占用的内存也越来越多。极端情况下可能会导致内存溢出。
可以对队列的大小做出限制,或者使用直接交接队列SynchronousQueue,那么便会创建不多于最大线程数的线程,且又不会无限的缓存任务。
线程池大小
cpu密集型:
可以考虑N+1,考虑+1是可能会有缺页中断,导致线程等待。
io密集型:
可以设置的相对大一点,因为等待io的时间会比使用cpu的时间长,处于io等待状态的线程并不会消耗cpu资源,例如2n。
对于执行io操作的线程越多,引起上下文切换也就越多,因此对于io密集型的任务,核心线程数可以设为1,最大线程数可以设为2N,这样导致的上下文切换也是最少的。
五、线程泄露
run方法没有异常捕获,导致run方法意外返回,使线程提前终止。
或者run方法一直处于等待状态,没有设置超时。
六、队列
6.1 非阻塞队列
当队列为空时返回异常或者null
ConcurrentHashMap:支持并发操作的map,不支持排序。
ConcurrentSkipListMap:支持并发操作,且支持排序。需要对象实现Comparable接口,并重写compareTo方法,实现排序,返回-1表示小,返回1表示大,返回0表示相等。
ConcurrentSkipListSet:支持并发和排序,且不允许数据重复。需要对象实现Comparable接口,并重写compareTo方法,实现排序,返回-1表示小,返回1表示大,返回0表示相等。重写hashCode方法,和equals。
ConcurrentLinkedQueue:提供了并发的队列,仅支持对头的操作。
ConcurrentLinkedDequeue:支持对头尾的双向操作。
CopyOnWriteArrayList:由于ArrayList是线程不安全的,可以使用它代替。
CopyOnWriteArraySet:可以解决HashSet不安全的问题。
6.2阻塞队列
当队列为空,或者队列的容量满的时候,会使线程等待,直到队列中有数据或者队列有空间的时候,线程会被唤醒。
ArrayBlockingQueuq:没有空间时,put()存放数据会阻塞,take()取数据时,队列为空,会阻塞。
new ArrayBlockQueue(10,true);可以使用公平锁和非公平锁。true为公平锁,false为非公平锁。
LinkedBlockingQueue:和ArrayBlockingQueue大体上一样,但是ArrayBlockingQueue要比LinkedBlockingQueue效率高。LinkedBlockingQueue支持队列头部的操作。
SynchronousQueue:是一种阻塞队列,没有容量。