java多线程(五):线程池详解

1、线程池的优势:

  • 重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销。
  • 能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的阻塞现象。
  • 能对线程进行简单的管理。并提供定时执行以及指定间隔循环执行等功能。

ThreadPoolExecutor的构造方法

可以通过ThreadPoolExecutor来创建一个线程池,ThreadPoolExecutor类一共有4个构造方法。其中,拥有最多参数的构造方法:

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

参数说明:

  • corePoolSize: 线程池核心线程数。默认情况下,核心线程会在线程池中一直存活,即使他们处于闲置状态。如果当前运行的线程数少于corePoolSize,则创建新线程来处理任务;如果当前运行的线程数等于或多于corePoolSize,则不再创建新的线程。
  • maxmumPoolSize: 线程池允许创建的最大线程数。如果任务队列满了并且线程数小于maximumPoolSize时,则线程池仍然会创建新的线程来处理任务。
  • keepAliveTime: 非核心线程闲置的超时时间。超过这个时间,非核心线程会被回收。如果allowCoreThreadTimeout设置为true,核心线程也会被回收。
  • TimeUnit: keepAliveTime参数的时间单位。这是一个枚举,有Days(天)、HOURS(小时)、MINUTES(分钟)、MILLISECONDS(毫秒)、SECONDS(秒)等。
  • workQueue: 任务队列。如果当前线程数大于corePoolSize,则将任务添加到此任务队列中。该任务队列是BlockingQueue类型的,也就是阻塞队列。存储在这里的任务是由ThreadPoolExecutorexecute方法提交来的。
  • threadFactory: 线程工厂。为线程池提供创建新线程的功能,一般情况下无须设置该参数。
  • RejectedExecutionHandler: 饱和策略。这是当前任务队列和线程池都满了时所采取的应对策略,默认是AbordPolicy,表示无法处理新任务,并抛出RejectedExecutionException异常。

RejectedExecutionHandler:饱和策略(共4种):
1.AbordPolicy:无法处理新任务,并抛出RejectedExecutionException异常。
2.CallerRunsPolicy:用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
3.DiscardPolicy:不能执行的任务,并将该任务删除。
4.DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。

线程池的原理,当提交一个新的任务到线程池时,线程池的处理流程如下:
java多线程(五):线程池详解_第1张图片

2、执行

ThreadPoolExecutor有两个方法可以供我们执行,分别是submit()和execute(),我们先来看看这两个方法到底有什么差异。

execute()方法源码:

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();
        }
        //判断线程池是否为RUNNING状态,并且将任务添加至队列中.
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //审核下线程池的状态,如果不是RUNNING状态,直接移除队列中
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //如果当前线程数量为0,则单独创建线程,而不指定任务.
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果不满足上述条件,尝试创建一个非核心线程来执行任务,如果创建失败,调用reject()方法.
        else if (!addWorker(command, false))
            reject(command);
    }

submit()方法源码:

public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        //还是通过调用execute
        execute(ftask);
        //最后会将包装好的Runable返回
        return ftask;
    }

    //将Callable 包装进FutureTask中
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

//可以看出FutureTask也是实现Runnable接口,因为RunableFuture本身就继承了Runnabel接口
public class FutureTask<V> implements RunnableFuture<V> {
    .......
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

所以,通常情况下,在不需要线程执行返回结果值时,我们使用execute 方法。 而当我们需要返回值时,则使用submit方法,他会返回一个Future对象。Future不仅仅可以获得一个结果,他还可以被取消,我们可以通过调用future的cancel()方法,取消一个Future的执行。 比如我们加入了一个线程,但是在这过程中我们又想中断它,则可通过sumbit 来实现。

3、简单例子

//创建基本线程池
final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,1,TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(50));
                
for (int i = 0; i < 30; i++) {
     final int finali = i;
     Runnable runnable = new Runnable() {
             @Override
              public void run() {
                   try {
                        Thread.sleep(3000);
                        Log.e("TAG","run : "+finali+"  当前线程:"+Thread.currentThread().getName());
                   } catch (InterruptedException e) {
                         e.printStackTrace();
                   }
               }
      };
      
     threadPoolExecutor.execute(runnable);
}

结果:
每3s打印三次日志。

4、线程池的分类

FixedThreadPool (可重用固定线程数的线程池)

//源码实现
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
//创建及执行
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
//执行上述Demo的runnable
fixedThreadPool.execute(runnable);

结果:每3s打印5次任务,跟上面的基础线程池类似。
特点:参数为核心线程数,只有核心线程,无非核心线程无超时时长,并且阻塞队列无界。
适用:执行长期的任务,性能好很多

SingleThreadPool(单个工作线程的线程池)

//源码实现
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }//创建及执行
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
//执行上述Demo的runnable
singleThreadExecutor .execute(runnable);

结果:每3s打印1次任务。
特点:只有一个核心线程,当被占用时,其他的任务需要进入队列等待,fixedThreadPool设置核心线程为1时就是SingleThreadPool。
适用:一个任务一个任务执行的场景

CachedThreadPool (根据需求创建线程的线程池)

//源码实现
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
//创建及执行
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
//执行上述Demo的runnable
cachedThreadPool.execute(runnable);

结果:3s后打印30次任务。
特点:没有核心线程,非核心线程无界,并且每个非核心线程空闲等待的时间为60s,采用SynchronousQueue队列。
结果分析

  • 因为没有核心线程,其他全为非核心线程,SynchronousQueue是不存储元素的,每次插入操作必须伴随一个移除操作,一个移除操作也要伴随一个插入操作。
  • 当一个任务执行时,先用SynchronousQueue的offer提交任务,如果线程池中有线程空闲,则调用SynchronousQueue的poll方法来移除任务并交给线程处理;如果没有线程空闲,则开启一个新的非核心线程来处理任务。当线程池中的线程空闲时,它会执行SynchronousQueue的poll方法,等待SynchronousQueue中新提交的任务。如果超过60s没有新任务提交到SynchronousQueue,则这个空闲线程将终止。
  • 由于maximumPoolSize是Integer.MAX_VALUE,无界的,所以如果线程处理任务速度小于提交任务的速度,则会不断地创建新的线程,这时需要注意不要过度创建,应采取措施调整双方速度,不然线程创建太多会影响性能。
  • 从其特点可以看出,CachedThreadPool适用于有大量需要立即执行的耗时少的任务的情况。

适用:适合执行大量需要立刻处理并且耗时较少的任务

ScheduledThreadPool(定时和周期性任务的线程池)

//源码实现
 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
    
//创建及执行
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
//延迟5秒执行
scheduledExecutorService.schedule(runnable, 5, TimeUnit.SECONDS);
//延迟5s后启动,每1s执行一次
scheduledExecutorService.scheduleAtFixedRate(runnable,5,1,TimeUnit.SECONDS);
//启动后第一次延迟5s执行,后面延迟1s执行
scheduledExecutorService.scheduleWithFixedDelay(runnable,5,1,TimeUnit.SECONDS);

特点:核心线程数量是固定的,非核心线程无界。任务队列采用DelayedWorkQueue,是无界的阻塞队列,所以maximumPoolSize参数无效。ScheduledThreadPool也是四个线程池当中唯一一个具有定时定期执行任务功能的线程池。它适合执行一些周期性任务或者延时任务。
适用:执行一些周期性任务或者延时任务。

5、线程池中的任务的终止

一般线程执行完run方法之后,线程就正常结束了,线程池中的任务可以用下面的方式来实现

步骤:

  • 实现 Callable 接口
  • 调用 pool.submit() 方法,返回 Future 对象
  • 用 Future 对象来获取线程的状态。
private void cancelAThread() {
        ExecutorService pool = Executors.newFixedThreadPool(2);
          
          Callable<String> callable = new Callable<String>() {
              
            @Override
            public String call() throws Exception {
                System.out.println("test");
                return "true";
            }
        };
          
        Future<String> f = pool.submit(callable);
          
        System.out.println(f.isCancelled());
        System.out.println(f.isDone());
        f.cancel(true);
  
    }

6、线程池的其他常用方法

1.shutDown()  关闭线程池,不影响已经提交的任务

2.shutDownNow() 关闭线程池,并尝试去终止正在执行的线程

3.allowCoreThreadTimeOut(boolean value) 允许核心线程闲置超时时被回收

4.submit() 一般情况下我们使用execute来提交任务,但是有时候可能也会用到submit,使用submit的好处是submit有返回值。

5.beforeExecute() - 任务执行前执行的方法

6.afterExecute() -任务执行结束后执行的方法

7.terminated() -线程池关闭后执行的方法

7、如何配置线程池:

CPU密集型任务:
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

IO密集型任务:
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

混合型任务:
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

你可能感兴趣的:(Java,android,java)