前面的文章详细的介绍线程相关的内容,但在平时的开发工作中,我们很少去直接创建一个线程使用,一般都是通过线程池的方式来进行调用。这边文章就来介绍一下Java中的线程池是怎么工作的,以及各种线程池之间有什么区别
一、线程与线程池我们可以通过执行一段相同的代码,来看一下线程和线程池之间的区别
创建多个线程:
Long start = System.currentTimeMillis(); final Random random = new Random(); final Listlist = new ArrayList (); for (int i = 0; i < 100000; i++) { Thread thread = new Thread() { @Override public void run() { list.add(random.nextInt()); } }; thread.start(); } System.out.println("时间:" + (System.currentTimeMillis() - start));
时间:14729
线程池:
Long start = System.currentTimeMillis(); final Random random = new Random(); final Listlist = new ArrayList (); ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0; i < 100000; i++) { executorService.execute(new Runnable() { @Override public void run() { list.add(random.nextInt()); } }); } executorService.shutdown(); System.out.println("时间:"+(System.currentTimeMillis() - start));
时间:21
通过上面两种方式,我们明显的可以看出来,创建多个线程和使用线程池之间有明显的性能差别,造成这种情况的本质原因在于线程对于 *** 作系统来说是一个比较重的资源,它的创建和销毁都需要额外花费CPU很多时间。而线程池可以通过线程复用避免了大量创建线程时的消耗,从而实现高性能的目的
二、线程池创建Executors类提供这四种创建线程的,但这四种方式我们都不推荐使用,因为使用这种方式创建的线程池,线程池的相关参数都是采用默认的,很容易出现程序OOM的情况,所以我们更推荐使用new关键字来创建线程池,同时手动指定线程池的配置参数
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10); ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
我们可以看一下上面这四个方法的源码,就直到如何创建一个线程池了
-
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new linkedBlockingQueue
())); } -
newCachedThreadPool
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue
()); } -
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new linkedBlockingQueue
()); } -
newScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); } public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue
workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
从上面这几个这个方法的源码可以看出来,它们内部都是去New一个ThreadPoolExecutor实例,只是每种方法对应的参数不同而已,推荐直接使用这种方式来创建线程池。
三、线程池核心参数所有创建线程池的方式,最后都会调用到下面这个构造方法,我们按照该方法的入参进行介绍
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue3.1 corePoolSizeworkQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
核心线程数(常驻线程数),当线程池添加任务的时候,只要当前线程池中的线程数量没有达到核心线程数,都会创建一个新的线程来执行任务。当所有任务都执行完了之后,如果线程池中的线程数大于核心线程数,那么多出来的这部分线程,就会被销毁,而只保留核心线程数量的线程供后续使用。
3.2 maximumPoolSize顾名思义,就是线程池中允许同时存在的最大线程数,当任务数量大于核心线程数时,新的任务会先添加到队列中进行等待,当队列也满了的时候,就会去判断当前线程池的线程数是否大于线程池允许的最大线程数,如果小于最大线程数,就会为这些任务创建新的线程,而这些新的线程都是临时线程。
通过Executors创建线程池时,这个值通常被设置为Integer.MAX_VALUE,当任务量太多事,就会导致OOM
3.3 keepAliveTime&TimeUnit这个参数指定了临时线程的存活时间,TimeUnit参数指定时间的单位,将传进来的时间转换成纳秒。maximumPoolSize参数中说明了任务队列满了之后创建临时线程,之所以是临时线程,就是因为当线程池的任务都执行完了之后,这些临时的线程就会被销毁,那么什么时候销毁就是keepAliveTime参数决定的,超过这个时间后就会被销毁
同时这个参数也可以用于核心线程,如果线程池配置了allowCoreThreadTimeOut参数为true,表示如果核心线程等待时间超过了keepAliveTime,也会被回收
3.4 BlockingQueue阻塞队列,向线程池添加任务时,如果线程池中的线程数已经达到核心线程数,那么新添加进来的任务就会先缓存到阻塞队列中,当某个线程的任务执行完了之后,再从阻塞队列获取任务继续执行。
Executors类中的四种创建线程池的方法分别用到了linkedBlockingQueue、SynchronousQueue和DelayedWorkQueue
SynchronousQueue是一个同步队列,它并不会存储任务,当线程put一个任务后,该线程就会被阻塞,直到有线程调用take()方法,被阻塞的线程的才会被唤醒,它实现的是线程之间一对一传递消息的模型,而newCachedThreadPool()就是采用这种队列,就会导致一个现象就是,当线程数达到核心线程数后,当有新的任务进来后,就会被阻塞,需要创建一个新的线程来接收刚才的任务,否则就不能再添加任务。
linkedBlockingQueue是一种链表式的阻塞队列,但它是一个有界队列,我们通过newSingleThreadExecutor()方法创建线程池时,直接new linkedBlockingQueue
public linkedBlockingQueue() { this(Integer.MAX_VALUE); }
DelayedWorkQueue是一个延时队列,主要用在定时的线程池中,它提供了默认的初始化容量为16,并且可以扩容
private static final int INITIAL_CAPACITY = 16; private void grow() { int oldCapacity = queue.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50% if (newCapacity < 0) // overflow newCapacity = Integer.MAX_VALUE; queue = Arrays.copyOf(queue, newCapacity); }3.5 ThreadFactory
线程工厂,线程池中都是通过ThreadFactory的newThread()方法创建线程对象,Executors的四种的方法中,都是用DefaultThreadFactory作为线程工厂
3.6 RejectedExecutionHandler拒接策略,即当任务数填满了阻塞队列,并且当前线程数已经达到线程池规定的最大线程数时,再有新的任务进来,就要采用相应的拒绝策略来处理新的任务。
ThreadPoolExecutor中提供了四种拒绝策略:
CallerRunsPolicy:由调用线程池的execute()方法的线程来执行当前任务
AbortPolicy:直接抛出RejectedExecutionException异常
DiscardPolicy:忽略该任务,不做任何处理
DiscardOldestPolicy:移除任务队列中待的最久的任务,然后重新提交
ThreadPoolExecutor中使用AbortPolicy作为默认的拒绝策略,但在实际开发中,一般会采用自己去实现RejectedExecutionHandler接口,自定义拒绝策略
3.7 其他核心参数除了上面这些创建线程池时指定的参数外,线程池还有一些其他的核心参数,比如记录线程池状态、线程数量
ThreadPoolExecutor中通过INT类型的ctl属性来同时记录线程池状态和线程数量,其中高三位用来记录线程池状态,用低29位来记录线程池中线程数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int COUNT_BITS = Integer.SIZE - 3; private static final int CAPACITY = (1 << COUNT_BITS) - 1; // runState is stored in the high-order bits private static final int RUNNING = -1 << COUNT_BITS; private static final int SHUTDOWN = 0 << COUNT_BITS; private static final int STOP = 1 << COUNT_BITS; private static final int TIDYING = 2 << COUNT_BITS; private static final int TERMINATED = 3 << COUNT_BITS;
线程池状态的变迁图如下:
shutdown()和shutdownNow()方法区别在于:
线程池执行完shutdown()方法后,不能再继续向线程池添加任务,但已经添加的任务会继续执行;而执行完shutdownNow()方法之后,不仅不能向线程池添加任务,已经添加的任务也不会再执行,该方法内部会去中断所有线程
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)