多线程是java 一个很好的特性,多线程开发提交了用户的体验,不用等待这么久,但如果只是不停的创建线程,必然会带来很多问题,我们来看下面一个场境:如果服务器为了提交响应速度为每个请求创建一个线程,如果用户10000个用户,就必须创建10000个线程,这是一种浪费;而且在线程上面来回的切换也是非常浪费资源的,同时每个线程执行完就这样回收了。下次来又重新创建.因此java 就开发了,线程池来对线程进行管理,避免线程的重复创建和回收。
下面我们来看一下线程池的使用:
public class ExecutorTest {
private static ExecutorService executor ;
public static void main(String[] args) {
executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
executor.execute(new Runnable() {
@Override
public void run() {
for(int i =0;i< 9 ;i++) {
System.out.println(" test " + i);
}
}
});
//executor = new ThreadPoolExecutor(arg0, arg1, arg2, arg3, arg4, arg5);
}
}
首先:创建一个固定大小的线程池:Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()) 线程池最大线程数,取决于cpu支持的最大线程数 ,
然后;调用execute提交Runable任务。
<span style="font-size:18px;">Executors 静态方法提供给我们创建各种类型的线程,下面具体说一下:</span>
<span style="font-size:18px;"></span><p><strong><span courier="" new="" color:="" black="">1. newSingleThreadExecutor</span></strong></p><p>创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。</p><p><span courier="" new="" color:="" black="">2. <strong><span courier="" new="">newFixedThreadPool</span></strong></span></p><p>创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。</p><p><strong><span courier="" new="" color:="" black="">3. newCachedThreadPool</span></strong></p><p>创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,</p><p>那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。</p><p><span courier="" new="" color:="" black="">4. <strong><span courier="" new="">newScheduledThreadPool</span></strong></span></p><p>创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。</p>
jdk 文档中建议我们尽量使用以上静态方法来创建线程,但如果具体使用过程 发现上面线程池不能满足的时候,我们可以自己创建线程。下面我们来看一下;
private static ExecutorService executor ;
public static void main(String[] args) {
/*executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
executor.execute(new Runnable() {
@Override
public void run() {
for(int i =0;i< 9 ;i++) {
System.out.println(" test " + i);
}
}
});*/
executor = new ThreadPoolExecutor(5, 100, 0l,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory());
executor.execute(new Runnable() {
@Override
public void run() {
for(int i =0;i< 9 ;i++) {
System.out.println(" test " + i);
}
}
});
}
创建线程池的构造方法:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
corePoolSize指:线程池中存活的线程数,包括空闲线程,简单来说就是最大工作线程数,
maximumPoolSize指:线程池中允许的最大线程数. 一般maximumPoolSize> corePoolSize
keepAliveTime:当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间;
unit:keepAliveTime的单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
threadFactory - 执行程序创建新线程时使用的工厂,一般都有默认创建进程的defaultThreadFactory
handler - 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序
keepAliveTime 这个开始有点难理解;如果开始的时候我们提交了,5个任务(Runable),线程池就会为我们创建5个线程,后面我们如果只提交3个任务,那么就会有2个线程进入空闲状态,那么要不要回收这个空闲的线程,取决我们设置的时间,如果马上回收下次任务来的时候又要重新创建,不回收又占用资源,所以我们设置的时间一定要慎重。
下面我们来看一下workQueue队列的策略:
排队有三种通用策略:
直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
下面分别来解释一下三种队列的使用,以及遇到的各种情况:
我们先创建有SynchronousQueue队列的线程池:new ThreadPoolExecutor(5, 100, 0l,
TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), Executors.defaultThreadFactory());
SynchronousQueue队列有一个特性:在某次添加元素后必须等待其他线程取走后才能继续添加
1.如果创建线程达到corePoolSize(5),提交之后直接添加到队列SynchronousQueue中,
2.如果再提交任务,如果队列中没有的任务没有被取走,刚直接创建线程来运行
3.如果创建的线程数已经到达maximumPoolSize(100),此时没有办法创建进程来运行,队列又无法添加,所以只能进入Handler(异常处理).
总结:SynchronousQueue策略直接创建线程直到达到最在线程,这样会造成资源过度浪费。
LinkedBlockingQueue无界队列的策略:
先创建无界队列: new ThreadPoolExecutor(5, 100, 0l,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory());
1.如果创建的线程没有达到corePoolSize(5),则下次提交则直接创建线程。
2.如果此时线程已经达到corePoolSize(5),则再次提交时,添加到队列中。
3.如果由于资源问题或者其他问题无法添加到队列中,再提交任务时,则会创建线程运行;不过这些情况一般不会存在,所以当到达corePoolSize线程数时候 ,不会再创建线程,所以maximumPoolSize基本没有什么作用。
总结:LinkedBlockingQueue 队列的特点:所以任务都会添加到队列中,排队执行;缺点:如果添加到线程池的各个任务执行时间不一样长的话,过长的执行时间会造成其他线程等待时间过长。
ArrayBlockingQueue 有界队列的策略:
先创建有界队列:new ThreadPoolExecutor(5, 100, 0l,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(100), Executors.defaultThreadFactory());
1.如果创建的进程没有达到corePoolSize(5),则下次提交会直接创建线程运行。
2.如果达到corePoolSize(5),则会添加到队列中。
3.如果队列也满了,则创建新的线程来运行。
4.如果创建进程到达maximumPoolSize数,则调用Handler进行异常处理。
总结:ArrayBlockingQueue能避免资源的过度浪费,但corePoolSize和队列的大小 很难控制。
再讲一下Handler异常处理:
自定义型:
RejectedExecutionHandler
RejectedExecutionHandler接口提供了对于拒绝任务的处理的自定方法的机会。如果想自己处理线程池满的情况,可以使用这个。
直接运行型:
CallerRunsPolicy:线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制直接执行任务Runable的run方法,能够减缓新任务的提交速度。
这个策略显然不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。
丢弃型:
AbortPolicy:处理程序遭到拒绝将抛出运行时 RejectedExecutionException这种策略直接抛出异常,丢弃任务。
DiscardPolicy:不能执行的任务将被删除 这种策略和AbortPolicy几乎一样,也是丢弃任务,只不过他不抛出异常。
重试型:
DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)
该策略就稍微复杂一些,在pool没有关闭的前提下首先丢掉缓存在队列中的最早的任务,然后重新尝试运行该任务。这个策略需要适当小心。