java线程池

一.概述

在开发工程中,通常通过new Thread()方法创建新的线程来执行耗时操作,这种做法有许多弊端。我们无法管理这些所创建的线程,如果无限制的创建过多的线程,可能会引起占用过多的资源而卡死。更好的解决方案就是合理利用线程池,线程池优点有:
1.降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
2. 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
3. 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
4.更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

二.线程池的使用

我们可以通过线程池ThreadPoolExecutor构造函数来创建线程池,还可以利用Executors类提供的4个不同线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor来创建

1. ThreadPoolExecutor构造函数创建

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


ThreadPoolExecutor参数含义
corePoolSize

线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲置的核心线程就会被终止。

maximumPoolSize

线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非核心线程数。

keepAliveTime

非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线程产生效果。

unit

keepAliveTime的单位
TimeUnit是一个枚举类型,其包括:

NANOSECONDS : 1微毫秒 = 1微秒 / 1000

MICROSECONDS : 1微秒 = 1毫秒 / 1000

MILLISECONDS : 1毫秒 = 1秒 /1000

SECONDS : 秒

MINUTES : 分

HOURS : 小时

DAYS : 天

workQueue

默认情况下,任务进来之后先分配给核心线程执行,核心线程如果都被占用,并不会立刻开启非核心线程执行任务,而是将任务插入任务队列等待执行,核心线程会从任务队列取任务来执行,任务队列可以设置最大值,一旦插入的任务足够多,达到最大值,才会创建非核心线程执行任务。

常见的workQueue有四种:

  1. SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
    2.LinkedBlockingQueue:这个队列接收到任务的时候,如果当前已经创建的核心线程数小于线程池的核心线程数上限,则新建线程(核心线程)处理任务;如果当前已经创建的核心线程数等于核心线程数上限,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
  2. ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误,或是执行实现定义好的饱和策略
  3. DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务
threadFactory

线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。 下面是Executors类里的默认线程工厂方法的源码。

static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

从上面可以看出默认线程工厂创建出的是一个非守护、优先级为Thread.NORM_PRIORITY 的线程。如果想要自己定制线程工厂满足需求,只需实现ThreadFactory接口的Thread newThread(Runnable r)方法。

handler

它是RejectedExecutionHandler对象,而RejectedExecutionHandler是一个接口,里面只有一个rejectedExecution方法。当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。在ThreadPoolExecutor中有四个内部类实现了RejectedExecutionHandler接口。在线程池中它默认是AbortPolicy,在无法处理新任务时抛出RejectedExecutionException异常。下面是在ThreadPoolExecutor中提供的四个可选值。

  1. CallerRunsPolicy:只用调用者所在线程来运行任务。
  2. AbortPolicy:直接抛出RejectedExecutionException异常。
  3. DiscardPolicy:丢弃掉该任务,不进行处理
  4. DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

我们也可以通过实现RejectedExecutionHandler接口来自定义我们自己的handler。如记录日志或持久化不能处理的任务。

1.1 ThreadPoolExecutor使用
ExecutorService executorService = new ThreadPoolExecutor(5,10,10,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>());

构造方法中的其他参数都采用默认值。可以通过execute和submit两种方式来向线程池提交一个任务。

execute

当我们使用execute来提交任务时,由于execute方法没有返回值,所以说我们也就无法判定任务是否被线程池执行成功。

executorService.execute(new Runnable() {

    @Override
    public void run() {
     Loge.e(TAG,"execute方式")
    }
});
submit

当我们使用submit来提交任务时,它会返回一个future,我们就可以通过这个future来判断任务是否执行成功,还可以通过future的get方法来获取返回值。如果子线程任务没有完成,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时候有可能任务并没有执行完。

 Future<Integer> submit = cachedThreadPool.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                Loge.e(TAG,"submit方式")
                return 2;
            }
        });
        try {
            Integer integer = submit.get();
        } catch (InterruptedException e) {
            // 处理中断异常
            e.printStackTrace();
        } catch (ExecutionException e) {
            // 处理无法执行异常
            e.printStackTrace();
        }
线程池关闭

调用线程池的shutdown()或shutdownNow()方法来关闭线程池

shutdown原理:将线程池状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

shutdownNow原理:将线程池的状态设置成STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行任务的列表。

中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。

2. newFixedThreadPool

通过Executors中的newFixedThreadPool方法来创建

 ExecutorService executorService = Executors.newFixedThreadPool(4);
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                Log.e(TAG, "newFixedThreadPool方式" );
            }
        });

在这个线程池中所容纳最大的线程数就是我们设置的核心线程数。如果线程池的线程处于空闲状态的话,它们并不会被回收,除非是这个线程池被关闭。如果所有的线程都处于活动状态的话,新任务就会处于等待状态,直到有线程空闲出来。

由于newFixedThreadPool只有核心线程,并且这些线程都不会被回收,也就是它能够更快速的响应外界请求。从下面的newFixedThreadPool方法的实现可以看出,newFixedThreadPool只有核心线程,并且不存在超时机制,采用LinkedBlockingQueue,所以对于任务队列的大小也是没有限制的。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

3. newCachedThreadPool

通过Executors中的newCachedThreadPool方法来创建。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

通过上面的newCachedThreadPool方法在这里我们可以看出它的核心线程数为0,线程池的最大线程数Integer.MAX_VALUE。而Integer.MAX_VALUE是一个很大的数,也差不多可以说这个线程池中的最大线程数可以任意大。

当线程池中的线程都处于活动状态的时候,线程池就会创建一个新的线程来处理任务。该线程池中的线程超时时长为60秒,所以当线程处于闲置状态超过60秒的时候便会被回收。这也就意味着若是整个线程池的线程都处于闲置状态超过60秒以后,在newCachedThreadPool线程池中是不存在任何线程的,所以这时候它几乎不占用任何的系统资源。

对于newCachedThreadPool他的任务队列采用的是SynchronousQueue,上面说到在SynchronousQueue内部没有任何容量的阻塞队列。SynchronousQueue内部相当于一个空集合,我们无法将一个任务插入到SynchronousQueue中。所以说在线程池中如果现有线程无法接收任务,将会创建新的线程来执行任务。

4. newScheduledThreadPool

通过Executors中的newScheduledThreadPool方法来创建。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}
  • schedule(Runnable command, long delay, TimeUnit unit):延迟一定时间后执行Runnable任务;

  • schedule(Callable callable, long delay, TimeUnit unit):延迟一定时间后执行Callable任务;

  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):延迟一定时间后,以间隔period时间的频率周期性地执行任务;

  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit):与scheduleAtFixedRate()方法很类似,但是不同的是scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的。

         ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                Log.e(TAG, Thread.currentThread().getName() );
            }
        }, 3, 1, TimeUnit.SECONDS);

执行结果

03-16 08:53:53.665 7002-7023/com.zs.myexecutor E/MainActivity: pool-1-thread-1
03-16 08:53:54.624 7002-7023/com.zs.myexecutor E/MainActivity: pool-1-thread-1
03-16 08:53:55.624 7002-7023/com.zs.myexecutor E/MainActivity: pool-1-thread-1
03-16 08:53:56.624 7002-7023/com.zs.myexecutor E/MainActivity: pool-1-thread-1
03-16 08:53:57.624 7002-7023/com.zs.myexecutor E/MainActivity: pool-1-thread-1
03-16 08:53:58.624 7002-7023/com.zs.myexecutor E/MainActivity: pool-1-thread-1
03-16 08:53:59.624 7002-7023/com.zs.myexecutor E/MainActivity: pool-1-thread-1

5. newSingleThreadExecutor

通过Executors中的newScheduledThreadPool方法来创建。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

通过Executors中的newSingleThreadExecutor方法来创建,在这个线程池中只有一个核心线程,对于任务队列没有大小限制,也就意味着这一个任务处于活动状态时,其他任务都会在任务队列中排队等候依次执行。

newSingleThreadExecutor将所有的外界任务统一到一个线程中支持,所以在这个任务执行之间我们不需要处理线程同步的问题。

三.线程池执行流程

image

  • 如果在线程池中的线程数量没有达到核心的线程数量,这时候就回启动一个核心线程来执行任务。

  • 如果线程池中的线程数量已经超过核心线程数,这时候任务就会被插入到任务队列中排队等待执行。

  • 由于任务队列已满,无法将任务插入到任务队列中。这个时候如果线程池中的线程数量没有达到线程池所设定的最大值,那么这时候就会立即启动一个非核心线程来执行任务。

  • 如果线程池中的数量达到了所规定的最大值,那么就会拒绝执行此任务,这时候就会调用RejectedExecutionHandler中的rejectedExecution方法来通知调用者。

四. 合理的配置线程池

要想合理的胚子线程池,就必须首先分析任务特性,可以从一下价格角度来进行分析:

  1. 任务的性质:CPU密集型任务,io密集型任务和混合型任务
  2. 任务的优先级:高,中,低
  3. 任务的执行时间:长,中,短
  4. 任务的依赖性:是否依赖其他系统支援,如数据库连接。
  • 任务性质不同的任务可以用不同规模的线程池分开处理,CPU密集型任务配置尽可能的少线程数量,如配置N+1个线程的线程吃池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行,则配置尽可能多的线程,如2*N。混合型任务,如果可以拆分则将其分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不太大,那么分解后执行的吞吐率要高于串行的吞吐率。如果这两个任务执行的时间相差太大,则没必要分解。我梦可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

  • 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先得到执行,需要注意的时如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永不执行。

  • 执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的先执行。

  • 依赖数据库连接池的任务,因为线程体检SQL后需要等待数据库返回结果,如果等待时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU

  • 建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。

五.线程池的监控

通过线程池提供的参数进行监控。线程池里有些属性在监控线程池的时候可以使用

  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
  • largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
  • getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。

protected void beforeExecute(Thread t,Runnable r){}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值