线程池

如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在处理实际的用户请求的时间和资源要多得多。

除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个JVM里创建太多的线程,可能会导致系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当地调整线程池中的线程数目可以防止出现资源不足的情况。 

Java中的线程池ThreadPoolExecutor

1、ThreadPoolExecutor

首先线程池有几个关键的配置:核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略

工作流程:

  • 默认情况下线程不会预创建,所以是来任务之后才会创建线程(设置prestartAllCoreThreads可以预创建核心线程)。
  • 当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。
  • 如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。
  • 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略。
  • 如果线程空闲时间超过空闲存活时间,并且线程线程数是大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置allowCoreThreadTimeOut 可以回收核心线程)。

1、1 工作队列

线程池

用来存储Runnable

线程池

上面的是继承了这个阻塞队列的。

我们可以选一个来看:ArrayBlockingQueue:

public ArrayBlockingQueue(int capacity, boolean fair) {         if (capacity <= 0)             throw new IllegalArgumentException();         this.items = new Object[capacity];         lock = new ReentrantLock(fair);         notEmpty = lock.newCondition();         notFull =  lock.newCondition();     }

第一个参数是用来装Runnable的数组大小,第二个参数是是否公平,如果是公平的话,就是正常的队列,先来先运行。非公平的话,就会有很多种情况。通常是用来设置ReentrantLock中的公平锁和非公平锁。

notEmpty = lock.newCondition();    //当数组为空的时候,代表着队列中没有任务,要等待任务进入队列,
notFull =  lock.newCondition();     //当数组满的时候,任务不能进入队列,就要进行这个信号量的等待。

我们在分析一下:任务怎样添加到队列:

public void put(E e) throws InterruptedException {         checkNotNull(e);         final ReentrantLock lock = this.lock;         lock.lockInterruptibly();         try {             while (count == items.length)                 notFull.await();             enqueue(e);         } finally {             lock.unlock();         }     }

如果当前数组内元素的个数等于数组长度的话,也就表示着队列已经放满了,不能再放入任务,这个时候,我们的notFull就要进行等待。我们先不管notFull什么时候signal(释放),

调用了enqueue(任务进队):

private void enqueue(E x) {         // assert lock.getHoldCount() == 1;         // assert items[putIndex] == null;         final Object[] items = this.items;         items[putIndex] = x;         if (++putIndex == items.length)             putIndex = 0;         count++;         notEmpty.signal();     }

这里有一个notEmpty.signal:因为添加了一个任务,表示有任务可以消费了,这样就要通知一下出队操作。

public E take() throws InterruptedException {         final ReentrantLock lock = this.lock;         lock.lockInterruptibly();         try {             while (count == 0)                 notEmpty.await();             return dequeue();         } finally {             lock.unlock();         }     }

如果队列中没有任务的话,我们就要进行等待。notEmpty.await()等待。如果有任务的话,就要进行出队dequeue()。

private E dequeue() {         // assert lock.getHoldCount() == 1;         // assert items[takeIndex] != null;         final Object[] items = this.items;         @SuppressWarnings("unchecked")         E x = (E) items[takeIndex];         items[takeIndex] = null;         if (++takeIndex == items.length)             takeIndex = 0;         count--;         if (itrs != null)             itrs.elementDequeued();         notFull.signal();         return x;     }

当任务出队了,这个时候表示队列可以存放元素了。notFull.signal()。

1、2 拒接策略

通常有四种:

线程池

我们从名字上都可以线程数达到最大线程跟队列满的时候的一些策略:

比如:

1、CallerRunsPolicy:线程数达到最大线程跟队列满的时候,调用调用者线程来执行这个任务,比如再main线程中开启的这个线程池,就会让main线程来执行这个任务。

线程池

我们也是可以看到是r.run(),通常要启动一个线程我们都是r.start()的,但是这个不是,就是正常的方法调用。

2、AbortPolicy: 线程数达到最大线程跟队列满的时候,直接抛出异常。

线程池

直接抛出异常。

3、DiscardPolicy:线程数达到最大线程跟队列满的时候,直接抛弃想要添加的任务。

线程池

不进行任何操作,也就是把新来的任务丢弃。

4、DiscardOldestPolicy:丢弃队列中最古老的任务。

线程池

丢弃原来的旧任务,然后添加新的任务,添加新任务的时候需要进行三步操作:

也就是execute()方法里面的三步判断:

线程池
  • 如果任务为null,直接抛出异常,
  • 如果当前工作的线程少于核心线程的话,就进行添加工作线程。
  • 如果一个任务可以成功排队,那么我们仍然需要仔细检查是否应该添加一个线程(因为现有线程自上次检查后就死掉了)或该池自进入此方法后就关闭了。因此,我们重新检查状态,并在必要时回滚排队(如果已停止),或者在没有线程的情况下启动新线程。
  • 3.如果我们无法将任务排队,则尝试添加一个新线程。如果失败,则表明我们已关闭或已饱和,因此拒绝该任务。

1、3 生产线程的工厂

线程池

使用的是默认的工厂。

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();         // 线程名前缀,例如 "pool-1-thread-"         namePrefix = "pool-" +                 poolNumber.getAndIncrement() +                 "-thread-";     }      /**      * 用于创建一个线程      */     public Thread newThread(Runnable r) {         Thread t = new Thread(group, r,                 namePrefix + threadNumber.getAndIncrement(),                 0);         // 设置线程t为非守护线程         if (t.isDaemon())             t.setDaemon(false);         // 设置线程t的优先级为5         if (t.getPriority() != Thread.NORM_PRIORITY)             t.setPriority(Thread.NORM_PRIORITY);         return t;     } }

2、线程运行的工作流程

1.线程池判断核心线程是否已经满了,否则会创建线程执行任务,是 进入下一个流程
2.线程池判断工作队列是否满了,否 把将要执行的任务加入队列,是 进入下一个流程
3.线程池判断线程池是否满了,否 创建线程执行任务,是进入下一个流程
4.线程池满了,按照策略处理无法执行的任务

线程池

通常是这样的工作流程。

3、常用的几个线程池

  • newFixedThreadPool:最大线程等于核心线程:队列是链表
线程池
  • newWorkStealingPool

jdk8出的。比较特殊。可以自己查看资料。

线程池
  • newSingleThreadExecutor
线程池

只有一个核心线程,但是当这个核心线程挂了的时候,会再生成另一个来进行代替。能保证任务是按顺序执行的。

  • newCachedThreadPool
线程池

能看到都是非核心线程,存活时间为60s。然后任务队列是没有存储空间的

  • newScheduledThreadPool
线程池

有核心线程和最大线程,并且不一定相等,延迟时间是通过延迟队列来进行的。

 

版权声明:玥玥 发表于 2021-04-02 0:55:35。
转载请注明:线程池 | 女黑客导航