Java 线程池详解(上)

Java 线程池详解(上),第1张

Java 线程池详解(上)

前面的文章详细的介绍线程相关的内容,但在平时的开发工作中,我们很少去直接创建一个线程使用,一般都是通过线程池的方式来进行调用。这边文章就来介绍一下Java中的线程池是怎么工作的,以及各种线程池之间有什么区别

一、线程与线程池

我们可以通过执行一段相同的代码,来看一下线程和线程池之间的区别

创建多个线程:

Long start = System.currentTimeMillis();
final Random random = new Random();
final List list = 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 List list = 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,
                          BlockingQueue workQueue,
                          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.1 corePoolSize

核心线程数(常驻线程数),当线程池添加任务的时候,只要当前线程池中的线程数量没有达到核心线程数,都会创建一个新的线程来执行任务。当所有任务都执行完了之后,如果线程池中的线程数大于核心线程数,那么多出来的这部分线程,就会被销毁,而只保留核心线程数量的线程供后续使用。

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()来创建阻塞队列,但这个阻塞队列默认的容量为Integer.MAX_VALUE,这也是导致程序出现OOM的原因之一,所以我们在创建阻塞队列的时候,一定要指定其容量。

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()方法之后,不仅不能向线程池添加任务,已经添加的任务也不会再执行,该方法内部会去中断所有线程

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5686821.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存