Java中的线程池
使用线程池的优点:降低资源消耗、提高响应速度、提高线程的可管理性。
线程池原理
线程池处理流程:
if (核心线程池没满){
//创建线程执行,执行完后会去工作队列中获取新的任务task.run()
}else if(工作队列没满){
//将提交的任务存储在工作队列里
}else if (线程池没满){
//创建线程执行任务
}else {
//其他策略处理无法执行的任务
}
对应下面的源码
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); 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); }
工作线程:线程池工作时会将线程封装成工作线程Worker,Worker在执行完任务后,会循环获取工作队列里的任务来执行。
构造函数
public ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue < Runnable > workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数:
corePoolSize,保存在池中的线程数。
maximumPoolSize,允许的最大线程数,如果队列满了,线程数小于最大线程数,将创建新线程。
keepAliveTime,如果线程数量大于核心数量,若超过这个时间还没有新的任务将会终止多余的空闲线程。
unit,时间单位。
workQueue,用于保存等待执行的任务的阻塞队列。
threadFactory,executor创建线程时使用的工厂。
handler,处理因为饱和无法执行任务的策略,包括直接抛异常、直接丢弃、丢弃队列里最近的任务、只用调用者所在的线程执行任务。
线程池的使用
在http://blog.youkuaiyun.com/qq407388356/article/details/78535703提到过线程池的一些用法,这里复习整合一下。使用线程池分别提交实现没有返回值的Runable接口和带有返回值的Callable<>接口。
在Java5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable<T>task) 方法来执行,并且返回一个 <T>Future<T>,是表示任务等待完成的Future。
Callable中的call()方法类似Runnable的run()方法,就是前者有返回值,后者没有。
当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。
同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。
import java.util.Random; import java.util.concurrent.*; /** * Created by SunLin on 2018.3.13 */ public class test_ThreadPool { public static void main(String[] args) throws ExecutionException, InterruptedException { Future<Integer>[] fts = new Future[5]; ExecutorService exec = Executors.newFixedThreadPool(10); for (int i = 0; i < 5; i++) { exec.execute(new RunnableTask1("Runnable" + i)); fts[i]=exec.submit(new RunnableTask2("Callable"+i)); //这句放在这里,将会阻塞循环运行,需要等到得到返回值fts[i].get()后才能再继续 //System.out.println("Callable"+i+"运行时间"+fts[i].get()); } for (int i=0;i<fts.length;i++){ System.out.println("Callable"+i+"运行时间"+fts[i].get()); } exec.shutdown(); } } class RunnableTask1 implements Runnable { private String threadName; RunnableTask1(String name) { threadName = name; System.out.println("Creating " + threadName); } public void run() { try { int time = new Random().nextInt(100); Thread.sleep(time*time); System.out.println("Running " + threadName+" 运行时间+"+time*time); } catch (InterruptedException e) { System.out.println(threadName + " interrupted."); } System.out.println("Thread " + threadName + " exiting."); } } class RunnableTask2 implements Callable<Integer>{ private String threadName; RunnableTask2(String name) { threadName = name; System.out.println("Creating " + threadName); } @Override public Integer call() throws Exception { int time = new Random().nextInt(100); Thread.sleep(time*time); System.out.println("Running " + threadName); return time*time; } }
execute和submit区别
1、接收的参数不一样
2、submit有返回值,而execute没有
3、submit方便Exception处理
如果你的task里会抛出checked或者unchecked exception,而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。
配置线程池考虑的因素
考虑一下因素:任务的性质(CPU密集、IO密集和混合型)、任务优先级、任务时间长短、任务依赖其他资源(如数据库)。
CPU密集型应配置尽可能小的线程,如配置CPU数量+1个线程的线程池。
IO密集型可以考虑2*CPU数量。
混合型的可以拆分成两个类型的任务。
优先级可以使用优先队列PriorityBlockingQueue来处理,可以让优先级高的任务先执行。
执行时间长短可以交给不同规模的线程池来处理,或者选用优先队列。
依赖资源(如数据库连接),可以配置尽量大的线程数量,以更好利用CPU资源。
同时可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法(这几个是空方法),可以对线程池进行监控。
Executor框架介绍
Executor框架主要包括三个部分:
任务
Runnable接口和Callable接口,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。
执行
核心接口Executor(将任务提交和执行分离),继承Executor的ExecutorService接口。
ThreadPoolExecutor(线程池的核心实现类,用来执行被提交的任务)和ScheduledThreadPoolExecutor(可以在给定的延迟后执行)实现了这个接口。
异步计算结果
Future接口和实现该接口的FutureTask实现类(通过ExecutorService.submit()返回该类型)。
ThreadPoolExecutor
ThreadPoolExecutor通常使用工厂类Executors来创建,是最核心的类。
可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。
SingleThreadExecutor
使用单个worker线程的线程池,相当于FixedThreadPool(1)。
FixedThreadPool
可重用固定线程数的线程池。corePoolSize和maximumPoolSize都设置为指定参数n,使用无界队列作为线程池工作队列(容量为Integer.MAX_VALUE)。
CachedThreadPool
根据需要创建新线程的线程池。corePoolSize设置为0,maximumPoolSize设置为Integer.MAX_VALUE。keepAliveTime设置为60L,表示空闲线程超过60秒会被终止。使用没有容量的SynchronousQueue作为工作队列。这个工作队列不是很好理解,没有容量,装载的不是等待执行的任务(因为任务不需要等待,maximumPoolSize=Integer.MAX_VALUE),而是空闲线程和提交任务的一个匹配。步骤如下:
1、首先执行线程execute()会执行SynchronousQueue.offer(Runnable task),如果当前有空闲线程正在执行SynchronousQueue.poll(),那么offer和poll操作配对成功,任务交给该worker线程执行。否则步骤2。
2、如果没有空线程,则没有poll操作来匹配,此时会创建一个新的线程来执行待执行的任务。
3、步骤2中新建的线程执行完后会执行SynchronousQueue.poll()操作,如果在这个操作中等待60秒没有出现offer操作的话,则视为空闲线程超过等待60而被终止。
总体可以这样理解:一个工地需要搬运工(worker线程),都在工地上(SynchronousQueue)等待搬运的工作(poll),如果来了任务(offer)则会调一个搬运工(worker)进行执行,而其他搬运工由于等待时间太久没工作干就散了,工作量增加时会再召集新的搬运工。
ScheduledThreadPoolExecutor
继承自ThreadPoolExecutor,使用工厂类Executors创建,主要用来给定的延迟之后运行任务,或者说定期执行任务。ScheduledThreadPoolExecutor功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大,Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor对应多个后台线程数。
调用scheduleAtFixedRate()或scheduleWithFixedDelay()后会向DelayQueue添加一个ScheduledFutureTask。线程池中线程从DelayQueue中获取一个Task执行。
ScheduledFutureTask主要包含3个成员变量:long time(被执行的具体时间)、long sequenceNumber(被添加后的序号)、long period(任务执行的间隔周期)。
DelayQueue封装了PriorityQueue,先比较time小的在前面,相等比较sequenceNumber小的在前(也就是执行时间一样,先提交的先执行)。
SingleThreadScheduledExecutor,则是具有单个的线程。
Future接口和FutureTask类
Future接口和实现类FutureTask类代表异步计算的结果。
FutureTask除了实现Future接口外还实现了Runnable接口。
当FutureTask处于未启动或已启动但未结束时,调用FutureTask.get()将导致调用线程阻塞(等待结果);当处于已完成状态时,执行FutureTask.get()将导致调用线程立即返回结果或抛出异常。
参考《Java并发编程的艺术》——方腾飞、魏鹏、程晓明
BlockingQueue、Semaphore、CountDownLatch、CyclicBarrier
l BlockingQueue
如果BlockingQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒,同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间时才会被唤醒继续操作。 BlockingQueue有四个具体的实现类,
ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小。其所含的对象是以FIFO(先入先出)顺序排序的。
LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定。其所含的对象是以FIFO顺序排序的。
PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。
l Semaphore
Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,acquire()获取一个许可,如果没有就等待,而release()释放一个许可。比如在Windows下可以设置共享文件的最大客户端访问个数。
l CountdownLatch
CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。(当每个线程调用countdown方法直到将countdownlatch方法创建时数减为0时,那么之前调用await()方法的线程才会继续执行。有一点注意,那就是只执行一次,不能到0以后重新执行)
l CyclicBarrier
类有一个整数初始值,此值表示将在同一点同步的线程数量。当其中一个线程到达确定点,它会调用await() 方法来等待其他线程。当线程调用这个方法,CyclicBarrier阻塞线程进入休眠直到其他线程到达。当最后一个线程调用CyclicBarrier 类的await() 方法,它唤醒所有等待的线程并继续执行它们的任务。(当等待的线程数量达到CyclicBarrier线程指定的数量以后(调用await方法的线程数),才一起往下执行,否则大家都在等待,注意:如果达到指定的线程数量的时候:则可以重新计数,上面的过程可以循环)
CountDownLatch是减计数方式,计数==0时释放所有等待的线程;CyclicBarrier是加计数方式,计数达到构造方法中参数指定的值时释放所有等待的线程。