Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具,它真正的线程池接口是ExecutorService。
使用线程池能够为了防止资源不足,因为频繁创建和销毁线程会消耗大量资源,尤其是当线程执行时间>线程创建时间+线程销毁时间,此时会堆积大量线程。Java中,创建线程池有四种方式,如下:
1)newCachedThreadPool()
创建一个可缓存线程池,线程池为无限大,如果线程池长度超过处理需要,可灵活回收空闲线程,即当执行当前任务时上一个任务已经完成,会复用执行上一个任务的线程,而不用每次新建线程;如果上一个线程没有结束则会新建线程。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPool {
private static ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
public static void main(String args[]) {
for (int i = 0; i < 10; i++) {
try {
if (i == 4) {
Thread.sleep(2000);
}
} catch (Exception e) {
e.getStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
} catch (Exception e) {
e.getStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在被执行");
}
});
}
}
/**
* 执行结果如下(console输出):
* pool-1-thread-1正在被执行
* pool-1-thread-4正在被执行
* pool-1-thread-3正在被执行
* pool-1-thread-2正在被执行
* pool-1-thread-4正在被执行
* pool-1-thread-1正在被执行
* pool-1-thread-3正在被执行
* pool-1-thread-2正在被执行
* pool-1-thread-5正在被执行
* pool-1-thread-6正在被执行
*/
}
在i=5的时候,需停留2秒,此时线程1-4都已经释放,所以1-4线程可以复用,这就是newCachedThreadPool()线程池的优点。但是这样的写法也会带来一个缺点,就是一旦线程无限增长,会导致内存溢出。
2)newFixedThreadPool()
可重用固定个数的线程池,当当前线程数大于总线程数时会进行等待,等待线程池内的线程执行完,相对来说占用内存少,因为如果等待线程数量过多,也是相对耗废资源的。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPool {
private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
public static void main(String args[]) {
for (int i = 0; i < 10; i++) {
fixedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "正在被执行");
try {
Thread.sleep(1000);
} catch (Exception e) {
e.getStackTrace();
}
}
});
}
}
}
线程池大小为4,所以如果线程执行数量超过4则会进行等待,所以newFixedThreadPool()的缺点也很明显,就是如果线程池数量设置明显过小,则会导致大量线程等待,造成资源浪费。所以一句话总结,缺点是:不支持自定义拒绝策略,大小固定,难以扩展。
3)newSingleThreadExecutor()
单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO,优先级)执行,如果该线程异常结束,会有另一个线程取代它,保证顺序执行。
所以缺点也很明显,就是单线程执行,线程多,执行速度会比较慢,所以不适合并发。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadPool {
private static ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
public static void main(String args[]) {
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadPool.execute(new Runnable() {
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "正在执行 index=" + index);
Thread.sleep(1000);
} catch (Exception e) {
e.getStackTrace();
}
}
});
}
}
/**
* 执行结果如下:
* pool-1-thread-1正在执行 index=0
* pool-1-thread-1正在执行 index=1
* pool-1-thread-1正在执行 index=2
* pool-1-thread-1正在执行 index=3
* pool-1-thread-1正在执行 index=4
* pool-1-thread-1正在执行 index=5
* pool-1-thread-1正在执行 index=6
* pool-1-thread-1正在执行 index=7
* pool-1-thread-1正在执行 index=8
* pool-1-thread-1正在执行 index=9
*
* 结论:所有的线程都是线程1来执行而且是有序执行,这就是单线程池!
*/
}
4)newScheduledThreadPool()
可重用固定个数的线程池,当前线程数大于总数则会进行等待,并且可以设置线程延迟执行时间。
优点:一个固定大小线程池,可以定时或周期性的执行任务
缺点:任务是单线程方式执行,一旦一个任务失败其他任务也受影响
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPool {
private static ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);
public static void main(String args[]) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmsssss");
for (int i = 0; i < 10; i++) {
System.out.println(i + "开始执行时间" + sdf.format(new Date()));
scheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行 执行时间=" + sdf.format(new Date()));
}
}, 3, TimeUnit.SECONDS);
}
}
}
那么,使用线程池会造成什么风险呢?
1)死锁:任何的多线程都有可能发生死锁的风险(线程之间互相等待)
2)资源不足:线程池太大造成,正常来说合理创建线程池大小一般不会出现这个问题
3)线程泄漏和请求过载
>>>>>阿里巴巴开发规范<<<<<
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool 和 newScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
所以,按照阿里巴巴的开发规范,实际编程中我们需要使用ThreadPoolExecutor创建线程池,它集以上优点于一身,比如:
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, // corePoolSize
100, // maximumPoolSize
100, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingDeque<>(100));// workQueue
for (int i = 0; i < 5; i++) {
final int taskIndex = i;
executor.execute(() -> {
System.out.println(taskIndex);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
参数解释:
corePoolSize : 线程池核心池的大小。
maximumPoolSize : 线程池的最大线程数。
keepAliveTime : 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit : keepAliveTime 的时间单位。
workQueue : 用来储存等待执行任务的队列。
threadFactory : 线程工厂。
handler 拒绝策略。
原理:
有请求时,创建线程执行任务,当线程数量等于corePoolSize时,请求加入阻塞队列里,当队列满了时,接着创建线程,线程数等于maximumPoolSize。 当任务处理不过来的时候,线程池开始执行拒绝策略。
阻塞队列:
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue: 一个不存储元素的阻塞队列。
LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
拒绝策略:
ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。 (默认)
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务。(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。