用法:
ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //线程存活时间
TimeUnit unit, //时间单位
BlockingQueue
RejectedExecutionHandler handler) //饱和策略
先用一个小例子感性地认识下线程池:
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
500, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(8),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 20; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(ai.getAndIncrement());
}
});
}
}
}
理解下线程池基本原理:
当提交一个新的任务到线程池时,处理流程如下:
1)判断核心线程池(corePool)里面还有没有空闲线程,如果还有空闲就创建新的工作线程来执行,如果没有就进入流程2;
2)判断阻塞队列(workQueue)是否已满,如果没满,就把新提交的任务放在阻塞队列里,如果满了就进入流程3;
3)判断线程池里忙碌的线程数是否达到最大(maximumPoolSize),如果还没有,就创建新的工作线程来执行,如果已经满了,就交给饱和策略处理。
1.阻塞队列有四种:
1)ArrayBlockingQueue:基于数组结构,必须指定容量capacity;
2)LinkedBlockingQueue:基于链表结构,可以指定容量capacity,也可以不指定,不指定就是Integer.MAX_VALUE;
3)SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入一直处于阻塞状态。
4)PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
2.饱和策略有四种:
1)AbortPolicy:直接抛出异常。注意了,这里虽然抛异常了,但是进程并没有结束。
继续使用上面示例的代码,我们知道最多能有8+8=16个线程,超过这个数字就拒绝执行并抛异常了,结果与预期符合,0-15都正常运行了,下一个线程不执行了:
看看这种策略的源码,在拒绝执行(rejectedExecution)时抛出了异常:
2)DiscardPolicy:直接丢弃掉任务,不执行也不报错。
看下示例代码,饱和策略换成DiscardPolicy后,丢失了打印16-19的任务。
看看源码,拒绝执行时啥也没做:
3)DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
正常执行打印0-15的任务,从16开始,丢弃最近的任务,所以16-19的打印被丢弃了。
源码:
4)CallerRunsPolicy:调用者执行
看看第一轮打印结果:打印了9个数字,但是线程池最大8,所以第9个数字是调用者线程(在这里例子中就是main线程执行的)。
这样做,main线程就不能干别的了。我们给线程池后面加个打印任务:
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
500, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(8),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 20; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(ai.getAndIncrement());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
System.out.println("main线程执行");
}
}
看到main线程被线程池征用了,第一轮打印0-8,第二轮打印9-17,第三轮就是main线程任务和打印18-20同时进行了。
源码:
有两种方法:execute()和submit。区别在与,execute无返回值,而submit返回Future,这样就可以通过Future的get()阻塞住。
只能提交Runnable任务,不能是Callable的:
可以提交Runnable任务
也可以提交Callable任务:
当运行Callable任务的时候,可以返回值,还可以抛异常
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
public static void main(String[] args) throws ExecutionException, InterruptedException {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
500, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(20),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 20; i++) {
Future future = threadPool.submit(new Callable() {
@Override
public String call() throws InterruptedException {
Thread.sleep(3000);
int val = ai.getAndIncrement();
return String.valueOf(val);
}
});
String val = future.get();
System.out.println(val);
}
}
}
观察输出结果可以看到数字是一个一个打印的,间隔3000ms,说明get()阻塞住了。
shutdown、shutdownNow
区别:
1)shutdownNow首先将线程池的状态设置为STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表。
2)shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断没有在执行的任务。
shutdown和shutdownNow无论执行哪个,isShutdown都会返回false,但是只有线程池真的关闭了,调用isTerminated才会返回true。
注意:
“中断”就是调用线程的interrupt方法,所以我们在线程池里起线程的时候不要捕获InterruptedException ,不然的话线程有可能停不下来,更蛋疼的是,不仅没停下来,而且还写了个死循环,鄙人之前搞出过大锅。举个例子吧:
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
500, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(20),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 20; i++) {
Future future = threadPool.submit(new Callable() {
@Override
public String call() {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("根本停不下来");
}
}
});
threadPool.shutdown();
}
}
}
future.get()可以设置超时机制:future.get(long timeout, TimeUnit unit),如果超时未执行完就会给线程抛一个中断标志,这个方法会有三个异常:
我们写个小例子:
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 8,
500, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(20),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 1; i++) {
Future future = threadPool.submit(new Callable() {
@Override
public String call(){
throw new NullPointerException("我抛的");
}
});
String val = null;
try {
val = future.get(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("超时异常");
} catch (ExecutionException e) {
System.out.println("内部任务异常");
System.out.println("实际异常是:" + e.getCause());
} catch (InterruptedException e){
System.out.println("中断异常");
}
System.out.println("666");
}
}
}
可以看到,内部的异常都被封装成了ExecutionException,不过我们仍然可以通过e.getCause()获取实际的异常:
Executors给我们封装了一些实现,简化线程池配置,下面讲解该框架提供的4种线程池:
newFixedThreadPool
newCachedThreadPool
newSingleThreadExecutor
newScheduledThreadPool
ExecutorService executor = Executors.newFixedThreadPool(4);
核心线程数和最大线程数相同,阻塞队列无限大(Integer.MAX_VALUE),所以任务再多,也能hold住,用起来就是火力全开,起nThreads个线程(最多这么多)。
开箱即用,拎包入住,只要设置一个线程池的大小就能用起来了。缺点也很明显,任务数量变得不可控,队列里缓存太多任务,搞不好就把内存干溢出了,所以阿里的java开发手册建议还是用ThreadPoolExecutor。
ExecutorService executor = Executors.newCachedThreadPool();
不用指定线程池大小,核心线程数0,最大线程数无穷大,阻塞队列用的SynchronousQueue,这种队列一个元素也不存,空闲线程竟然能存活60s之久,这种线程池简直就是性能小怪兽,吃内存和cpu不吐骨头。
执行新任务的时候也是先看看线程池里面有没有空闲的线程,有的话直接执行,没有就新建一个线程来执行。
ExecutorService executor = Executors.newSingleThreadExecutor();
如果说cachedThreadPool是小怪兽的话,那singleThreadExecutor简直就是个废物,因为它每次只执行一个任务。
核心线程数是1,最大线程数也是1,唯一的好处是阻塞队列是无界的,可以暂存Integer.MAX_VALUE个任务。
这种线程池存在的价值在于可以顺序执行一堆任务,那我不能用for循环执行吗,可以,但是不能异步执行。那我不能在for循环里new一个Thread吗,嗯好像也行,但是你没法控制一次有多少个线程执行呀。当然你又说了,那我可以用信号量控制呀,好吧也行,但是那样太复杂了,人生苦短,我们简单点就好。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(8);
(1)executor.schedule(Runnable command, long delay, TimeUnit unit)
延迟delay时间单位之后执行:
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(8);
for (int i = 0; i < 20; i++) {
executor.schedule(new Runnable() {
@Override
public void run() {
System.out.println(ai.getAndIncrement());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 5, TimeUnit.SECONDS);
}
}
}
(2)executor.scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
先等待initialDelay单位时间,然后每delay单位时间执行一次任务。
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(8);
for (int i = 0; i < 20; i++) {
executor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
System.out.println(ai.getAndIncrement());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 10, 3, TimeUnit.SECONDS);
}
}
}
CountDownLatch、CyclicBarrier、Semophore
先来个英语八级词汇:Latch,是啥?哈哈,有道查了下,是门闩的意思,很形象,想象一下,门上有两把门闩,俩都拉开的时候门就能开了。
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
private static CountDownLatch count = new CountDownLatch(4);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(ai.getAndIncrement());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.countDown();
}
}).start();
}
count.await();
System.out.println("门闩开啦,炒房团进来啦");
}
}
这个就跟前面介绍的Thread.join()类似,就是当前线程等待别的线程执行完了再执行。
原理就是await时加共享锁,countDown时释放共享锁。
又是一个八级词汇,论英语的重要性:循环使用的屏障
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
private static CyclicBarrier barrier = new CyclicBarrier(4, new Runnable() {
@Override
public void run() {
System.out.println("你们都执行完了吧?该轮到我了吧?");
}
});
public static void main(String[] args) {
for (int i = 0; i < 4; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(ai.getAndIncrement());
try {
Thread.sleep(3000);
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
}
可以在屏障里指定如果所有线程都到达屏障了,后面应该执行哪个线程。
信号量,控制并发量,分布式系统也有相似概念,称为“限流”。限流是有必要的,比如我们的系统里有一个导出汇总excel的功能,不限制并发量,系统立马就OOM了。
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
//我们新建一条三车道的高速公路
private static Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
//高速入口堵了20辆车
ExecutorService executors = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
executors.execute(new Runnable() {
@Override
public void run() {
try {
//放行一辆
semaphore.acquire();
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//车道空出来了
semaphore.release();
}
System.out.println(ai.getAndIncrement());
}
});
}
}
}
问:我们知道ThreadLocal可以用set、get存取线程私有变量,那如果是在线程池里使用ThreadLocal还能线程安全吗?
第一反应就是问题有坑,因为我们知道线程池中的线程是可以复用的,这个私有变量如果是上一个线程set的不就乱了吗,好我们来验证一下。
public class ThreadPoolTest {
private static AtomicInteger ai = new AtomicInteger();
private static final ThreadLocal threadLocal = new ThreadLocal(){
@Override
protected Integer initialValue() {
return -1;
}
};
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 8,
30, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
//空闲线程可以存活30s,我们趁热使用上一个线程
for (int i = 0; i < 4; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
int tmp = ai.getAndIncrement();
threadLocal.set(tmp);
System.out.println(Thread.currentThread().getId() + "#" + Thread.currentThread().getName() + "::"
+ threadLocal.get() + ", set::" + tmp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
for (int i = 0; i < 8; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + "#" + Thread.currentThread().getName() + "::"
+ threadLocal.get());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
所以线程池里的线程只要还存活着,那么设置的ThreadLocal变量值就还在。