线程池
线程池初始化时即创建一些空闲的线程。当程序将一个任务提交给线程池时,线程池就会选择一个空闲的线程来执行该任务。在任务结束以后,该线程并不会死亡,而是由线程池回收成为空闲状态,等待执行下一个任务。
线程池状态
线程池状态有如下几种(来源于网上):
RUNNING:运行时状态,可以接受新的任务,并处理等待队列中的任务
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务
STOP:不接受新的任务提交,也不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个
工作机制
线程池工作时,任务是提交给线程池。线程池接到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程;如果没有,则查看是否需要创建新的线程去处理。如果已达到最大线程数,则被暂存在一个任务队列中;如果是有限队列且满了,则执行拒绝策略。
整个流程源码在java.util.concurrent.ThreadPoolExecutor#execute.execute(Runnable command) 方法中:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
过程
线程池的构建
(图从网上找的)
创建线程池时有7个重要的参数:
int corePoolSize :初时线程池大小
int maximumPoolSize :最大线程数
long keepAliveTime:空闲多久回收线程
TimeUnit unit :时间单位
threadFactory :用来创建线程的工厂
BlockingQueue<Runnable> workQueue :任务队列
RejectedExecutionHandler handler:队列满了后,拒绝的callback
原生线程池
Java自带了7种线程池:
方法 | 最大线程数 | 队列 | 队列满了后 | 适用场景 |
newSingleThreadExecutor() | 1 | 无界 | / |
|
newCachedThreadPool() | Integer.MAX_VALUE | 无界 | / | 缓存线程,适合大量短时间任务 |
newFixedThreadPool(int nThreads) | 参数指定 | 无界 | / | 空闲则销毁,需要则补足 |
newSingleThreadScheduledExecutor() | 1 | DelayedWorkQueue |
| 返回ScheduledExecutorService,定时或周期性的工作调度 |
newScheduledThreadPool(int corePoolSize) | 参数指定 | DelayedWorkQueue |
| 同上,只是线程数不通而已 |
newWorkStealingPool(int parallelism) |
|
|
| Java8新加,Work-Stealing算法,并行地处理任务,不保证处理顺序 |
new ThreadPoolExecutor() | 参数指定 | 参数指定 | 参数指定 | 最基本的 |
队列
线程池队列分两种:有界队列、无界队列。也可以分阻塞队列与非阻塞队列:阻塞队列有SynchronousQueue、LinkedBlockingQueue、ArrayBlockingQueue与PriorityBlockingQueue,非阻塞队列有ConcurrentLinkedQueue与LinkedTransferQueue。
阻塞队列:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。生产者-消费者设计模式就是用的阻塞队列,这样可以简化编码。内部实现都是用锁实现。
非阻塞队列:一般使用CAS替代锁。
SynchronousQueue :即同步队列,是BlockingQueue 接口的实现。从名字可知,每次往队列里put 一个元素后,就会阻塞,直到有人task 该元素。
只是它是内部只能包含一个元素的队列。拥有公平(FIFO)和非公平(LIFO)策略,对应的就是两个内部类TransferQueue、TransferStack。
LinkedBlockingQueue :是一个无界队列,入对用的是putLock 锁,出对用的是takeLock 锁。
ArrayBlockingQueue :利用数组实现的一个队列,初始化时指定队列大小。
PriorityBlockingQueue:即优先级队列,意思就是可以控制任务的执行先后顺序。它是一个无界队列。因为有优先级,所以队列里的元素必须要实现comparable 接口。
ConcurrentLinkedQueue是使用CAS来实现非阻塞入队出队。
这里简单说下优先级队列的例子:
首先,需要缓存的对象需要实现Comparable接口
public class Event implements Comparable<Event> {
private int priority;
private String name;
public Event(String name, int priority) {
this.name = name;
this.priority = priority;
}
@Override
public int compareTo(Event event) {
return this.priority - event.priority;
}
public int getPriority() {
return priority;
}
public String getName() {
return name;
}
}
然后把元素加入队列,并用线程池去消费
public class TryPriorityBlockingQueue {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
PriorityBlockingQueue queue = new PriorityBlockingQueue<Event>();
queue.put(new Event("Third", 3));
queue.put(new Event("Fourth", 4));
queue.put(new Event("First", 1));
queue.put(new Event("Second", 2));
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit(new Runnable() {
@Override
public void run() {
while (!queue.isEmpty()) {
try {
Event event = (Event) queue.take();
System.out.println("event " + event.getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
countDownLatch.countDown();
}
});
countDownLatch.await();
executor.shutdown();
}
}
从输出结果看,确实是根据优先级进行消费的。
其实优先级队列本身是用堆实现的。
大顶堆:二叉树的父节点大于左右子节点
小顶堆:二叉树的父节点小于左右子节点
策略
线程池创建时,需要告诉线程池:当线程池处理不了新的任务时,怎么处理。Java默认提供了4种策略可供选择。
4个策略。分别是:
CallerRunsPolicy。提交任务的线程自己负责执行这个任务。
AbortPolicy。使Executor抛出异常,通过异常做处理。
DiscardPolicy。丢弃提交的任务。
DiscardOldestPolicy。丢弃掉队列中最早加入的任务
ForkJoin
ForkJoin 是Java 提供的并发处理框架。基本做法是分而治之,即把一个负责任务拆成多个小任务,然后把所有小任务的结果汇总。
核心类主要就是ForkJoinTask 抽象类与 ForkJoinPool。使用ForkJoin 时,只需要继承它的子类 RecursiveAction 或者 RecursiveTask 即可,并实现compute 方法。
复写实现 compute 方法一般需要遵循这个规则:
if(任务足够小){
直接执行任务;
如果有结果,return结果;
} else {
拆分为2个子任务;
分别执行子任务的fork方法;
分别执行子任务的join方法;
如果有结果,return合并结果;
}
例如:
public class MyForkJoinTask extends RecursiveTask<Integer> {
private List<Integer> needProcess;
private String start;
public MyForkJoinTask(String start, List<Integer> needProcess) {
this.start = start;
this.needProcess = needProcess;
}
@Override
protected Integer compute() {
try {
if (needProcess.size() < 5) {
return needProcess.stream().max((i, j) -> i - j).orElse(0);
}
int meddle = needProcess.size() / 2;
MyForkJoinTask left = new MyForkJoinTask(start + ".0", needProcess.subList(0, meddle));
MyForkJoinTask right = new MyForkJoinTask(start + ".1", needProcess.subList(meddle + 1, needProcess.size()));
left.fork();
right.fork();
Integer leftResult = left.get();
System.out.println("left " + left.start + " get " + leftResult);
Integer rightResult = right.get();
System.out.println("right " + right.start + " get " + rightResult);
return Math.max(leftResult, rightResult);
} catch (Exception e) {
System.out.println("failed");
}
return -1;
}
}
这里我利用task 分治取得最大值。对应的还要新建一个Pool
public class TestForkJoinTask {
public static void main(String[] args) {
List<Integer> input = Lists.newArrayList(14, 4, 69, 23, 90, 45, 78, 12, 37, 49, 48, 100, 24, 38, 49, 58, 29, 344, 1, 231, 124, 90, 38, 200, 125, 178, 158, 16, 159, 150, 25, 27, 49, 410, 387, 49, 37, 49, 151, 172, 25, 59, 289, 20);
ForkJoinPool forkJoinPool = new ForkJoinPool();
Integer max = forkJoinPool.invoke(new MyForkJoinTask("0", input));
System.out.println("Result is " + max);
}
}
此时就可以把比较路径输出了。
它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。因此线程数是可控的。
fork()方法类似于线程的Thread.start()方法,但是它不是真的启动一个线程,而是将任务放入到工作队列中。
join()方法类似于线程的Thread.join()方法,但是它不是简单地阻塞线程,而是利用工作线程运行其它任务。当一个工作线程中调用了join()方法,它将处理其它任务,直到注意到目标子任务已经完成了。
工作窃取
1.每个工作线程都有自己的工作队列WorkQueue;
2.这是一个双端队列,它是线程私有的;
3.ForkJoinTask中fork的子任务,将放入运行该任务的工作线程的队头,工作线程将以LIFO的顺序来处理工作队列中的任务;
4.为了最大化地利用CPU,空闲的线程将从其它线程的队列中“窃取”任务来执行;
5.从工作队列的尾部窃取任务,以减少竞争;
6.双端队列的操作:push()/pop()仅在其所有者工作线程中调用,poll()是由其它线程窃取任务时调用的;
7.当只剩下最后一个任务时,还是会存在竞争,是通过CAS来实现的;
说到这,就不得不提下Java 8的新特性parallelStream与parallel。它俩实现原理正是ForJoinPool。
如下代码:
@Test
public void should_print_sequence_1() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// 顺序输出
numbers.stream().forEach(System.out::println);
}
这里因为元素有序的,经过流依旧是顺序打印输出。改成下边就会出现乱序,即并行流:
// 乱序输出
numbers.parallelStream().forEach(System.out::println);
为了看清,我能把线程id输出:
numbers.parallelStream().forEach(s -> {
System.out.println("print " + s + " from " + Thread.currentThread().getId());
});
从输出发现线程ID确实不一致,也就是parallelStream 利用了多线程。
另外stream().parallel() 等于 parallelStream。即:
numbers.stream().parallel().forEach(System.out::println);
接下来,尝试加上 forEachOrdered
// 顺序输出
numbers.parallelStream().forEachOrdered(s -> {
System.out.println("print " + s + " from " + Thread.currentThread().getId());
});
从输出看,不再是并行。
再看下直接利用ForJoinPool:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
ForkJoinPool forkJoinPool = new ForkJoinPool();// 默认创建jvm处理器核数的并行度
forkJoinPool.submit(() -> {
numbers.parallelStream().forEach(s -> {
System.out.println("print " + s + " from " + Thread.currentThread().getId());
});
}).join();
注意:任务之间最好是状态无关的,因为parallelStream默认是非线程安全的。
另外,如果在lambda 表达式操作集合最好用 Collections.synchronizedList(new ArrayList<>()); 转成线程安全的。