java线程池的使用(1)

线程池简介


线程池技术一是用以控制系统中线程的数量,使得线程数不因太少而浪费资源,不因太多而导致效率不高(线程的创建和销毁都需要占用时间、资源)。
jdk1.4之前的版本中,线程池都极其简陋,jdk1.5之后引入了java.util.current之后,情况开始大幅改观。为我们解决线程问题提供了很大的便利。

java中线程池的接口


最顶级的接口是Excutors,但严格意义上说,Excutors并不是线程池,只是线程执行的工具,真正的线程池接口是ExcutorService。官方文档并不推荐使用构造方法创建线程池,因此我们一般使用Excutors一些方法来创建线程池:
1. newSingleThreadExecutor()
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2. newFixedThreadPool()
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3. newCachedThreadPool()
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4. newScheduledThreadPool()
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
下面看下各类关于它的具体实现

  /**
 *创建一个可重用固定数量线程的线程池
 *在任何时候至多有n个线程处于活动状态
 *如果在所有线程处于活动状态时提交其他任务,则它们将在队列中等待,
 *直到线程可用。 如果任何线程在关闭之前的执行期间由于失败而终止,
 *如果需要执行后续任务,则一个新的线程将取代它。池中的线程将一直存在
 *知道调用shutdown方法
 * @param nThreads 线程池中的线程数
 * @param threadFactory 创建新线程时使用的factory
 * @return 新创建的线程池
 * @throws NullPointerException 如果threadFactory为null
 * @throws IllegalArgumentException if {@code nThreads <= 0}
 ** /

 public static ExcutorService newFixedThreadPool(int nThreads){
     return new ThreadPoolExcutor(nThreads,nThreads,
                                 0L,TimeUnit.MILLIONSECONDS,
                                 new LinkedBlockingQueue<Runnable>);
 }

在这里,corePoolSize和maximumPoolSize相等,keepAliveTime为0,LinkedBlockingQueue是无界的。
而单线程池

/**
 *创建使用单个worker线程运行无界队列的Executor
 *并使用提供的ThreadFactory在需要时创建新线程
 *
 * @param threadFactory 创建新线程时使用的factory
 *
 * @return 新创建的单线程Executor
 * @throws NullPointerException 如果ThreadFactory为空
 */
public static ExcutorService newSingleThreadExcutor(){
    return new ThreadPoolExcutor(1,1,
                                 0L,TimeUnit.MILLIONSECONDS,
                                 new LinkedBlockingQueue<Runnable>);
}

newCachedThreadPool的实现


    //CachedThreadPool的corePoolSize被设置为空(0),maximumPoolSize被设置为Integer.MAX.VALUE,即它是无界的,
    //这也就意味着如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新的线
    //程。极端情况下,这样会导致耗尽cpu和内存资源。
    public static ExcutorService newCachedThreadPool(){
        return new ThreadPoolExcutor(0,Integer.MAX_VALUE,
                                     60L,TimeUnit.MILLIONSECONDS,
                                     new SynchronousQueue<Runnable>)
    }

ThreadPoolExcutor详解


ThreadPoolExcutor的构造方法的参数列表

我们所说的是java.util.current.ThreadPoolExecutor

//构造函数有4个
public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);

    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}
    /**
    *corePoolSize 线程池中保留的线程,包括闲置线程
    *maximumPoolSize 线程池中允许的最大线程数
    *keepAliveTime 当线程数大于核心数时,这是空闲线程等待新任务的最大时间
    *unit -keepAliveTime的时间单位
    *workingQueue 执行前用于保持任务的队列,此队列仅保持由excute()提交的Runnable任务
    *threadFactory 执行程序创建新线程的工厂
    *handler 由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序
    */

ThreadPoolExcutor继承自AbstractExecutorService,后者又实现了ExecutorService 接口,最后ExecutorService 接口又是Executor的子接口,Executor 是一个顶层实现,其中只有一个execute(Runnable command)方法,返回值为void,我们可以轻易地理解,该方法是负责执行传入的任务的

ThreadPoolExcutor中的重要方法
execute();
submit();
shutdown();
shutdownNow();

execute()Exuecutor 接口中声明的方法,在ThreadPoolExcutor 中进行了具体的实现,这个方法是ThreadPoolExcutor 的核心方法,通过这个方法可以向线程池提交一个任务,并由线程池去执行。
submit()是在ExecutorService中声明的方法,在AbstractExecutorService 中就有了具体的实现,ThreadPoolExcutor 中并没有对该方法进行重写,这个方法也是用来向线程池提交任务的,但是它和execute() 有所不同,它能够返回任务执行的具体结果,通过查看submit() 的源码可以看到,事实上它依然需要调用execute() 方法,只不过,它利用了Future来获取执行结果。
shutdown()shutdownNow()是用来关闭线程池的。
还有一些可以获取线程池相关属性的方法例如

        poolExecutor.allowsCoreThreadTimeOut();
        poolExecutor.getActiveCount();
        poolExecutor.getCorePoolSize();
        poolExecutor.prestartAllCoreThreads();

有兴趣的可以自行查阅。

深入剖析线程池的实现原理

线程池的状态

ThreadPoolExcutor 中有一个volatile型的字段来表示线程池的状态,此外还规定了几个静态变量整数对状态进行了规定。

volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

runstatevolatile 修饰来保证线程之间的可见性。当创建了线程池后,初始时,线程池处于RUNNING 状态;调用了shutdown()方法,则线程池处于相应的SHUTDOWN 状态,此时线程池不能接受新的任务,但是会等待所有任务执行完毕;如果调用了shutdownNow() 方法,那么线程池处于STOP 状态,此时线程池不再接受新的任务,并且尝试终止正在执行的任务,当线程池处于SHUTDOWN 或者STOP 状态,并且所有任务都被销毁,任务缓存队列清空或者执行完毕,线程池被置为TERMINATED 状态。

任务的执行

ThreadPoolExecutor一些重要的成员变量

private final BlockingQueue<Runnable> workQueue;            //任务缓存队列,用来存放等待执行的任务
//线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁
private final ReentrantLock mainLock = new ReentrantLock();   

private final HashSet<Worker> workers = new HashSet<Worker>();  //用来存放工作集

private volatile long  keepAliveTime;    //线程存活时间   
private volatile boolean allowCoreThreadTimeOut;   //是否允许为核心线程设置存活时间
//核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
private volatile int   corePoolSize;     
private volatile int   maximumPoolSize;   //线程池最大能容忍的线程数

private volatile int   poolSize;       //线程池中当前的线程数

private volatile RejectedExecutionHandler handler; //任务拒绝策略

private volatile ThreadFactory threadFactory;   //线程工厂,用来创建线程

private int largestPoolSize;   //用来记录线程池中曾经出现过的最大线程数

private long completedTaskCount;   //用来记录已经执行完毕的任务个数

这里其实从变量的名称上就可以大致地推测出用处,这里我们需要注意的是largestPoolSize 并不是一个可以设置的线程池的规模,它只是一个记录数
下面我们进入重点,从任务的提交到最终执行完毕到底经历了哪些过程。ThreadPoolExecutor 最核心的方法是execute() ,即使是submit()方法,最终还是需要调用execute(),所以我们有必要研究一下execute() 方法的实现。

//jdk8的情况
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);
}
  1. 其中的第一个条件,判断传入的任务是否为空,很好理解;
  2. 第二个条件if (workerCountOf(c) < corePoolSize),要判断当前的线程数是否不大于核心池的规模,如果不大于,那么就将用该任务创建一个Worker并且将Worker放入ThreadPoolExecutorworkers 属性(一个HashSet)中,结束execute()
  3. 进入下一个判断条件,如果线程池在运行中且任务成功地加入到了缓存任务队列中,继续进行double-check判断,前面我们已经检查过一次isRunning(c),这里我们做二次检查,这是因为这是多线程操作,没有进行同步,所以需要再次check操作,如果此时不处于运行状态并且从缓存队列中删除成功,那么拒绝执行该任务,结束execute(),否则判断当前线程数是否为0,是则添加进任务队列,否则结束execute()
  4. 如果在条件3中失败了,则直接添加线程,添加失败则拒绝该任务。

关于BlockingQueue


所有的BlockingQueue都可以用于传输和保持提交的任务,可以使用此队列和池大小进行交互:

  • 如果运行的线程少于线程池的corePoolSize,那么Excutor始终优先选择添加新的线程,而不进行排队;
  • 如果线程数大于或者等于线程池的corePoolSize,那么优先选择进入队列,而不添加线程;
  • 如果无法请求加入队列,则创建新的线程,除非线程数超过maximumPoolSize,这种情况下,任务会被拒绝。

排队的策略


  1. 直接提交:任务队列的默认选项是SynchronousQueue,它将任务直接提交给线程而不是保持它们。因此,如果不存在直接运行任务的线程,则试图将任务加入队列失效,因此会创建一个新的线程。此策略可以避免在处理具有内部依赖性的请求集时出现锁。直接提交应该要求无界maximumPoolSize以免拒绝任务提交。当命令以超过队列所能处理的平均数到达时,此策略允许无界线程线程具有增长的可能性。
  2. 无界队列:使用无界队列(例如不具有预定义容量的LinkedBlockingQueue)将导致所有corePoolSize线程都忙时新任务处在等待状态。这样创建的线程就不会超过corePoolSize,那么maximumPoolSize也就没有意义了。当每个任务之间相互独立时,适合于使用无界队列。当命令以超过队列处理的平均数到达时,此策略允许无界线程具有增长的可能性。
  3. 有界队列:当时用有界的maximumPoolSize时,使用有界队列(ArrayBlockingQueue)有助于避免资源的耗尽,但是较难调整和控制,队列大小和最大池大小可能需要相互折中:使用最大队列和小型池可以最大限度的降低CPU使用率,操作系统资源以及上下文切换的开销。但是可能也人工的造成了吞吐量的下降,使用小型池和大队列,则有可能增大了CPU的使用,但是又造成了不可接受的调度开销,同样会降低吞吐量。

参考文献
1.Java 线程池ThreadPoolExecutor(基于jdk1.8)(一)https://blog.csdn.net/youxitongyongming/article/details/77751874
深入理解Java之线程池

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值