添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

引言

线程池:可以理解为缓冲区,由于频繁的创建销毁线程会带来一定的成本,可以预先创建但不立即销毁,以共享方式为别人提供服务,一来可以提供效率,再者可以控制线程无线扩张。合理利用线程池能够带来三个好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 但是要做到合理的利用线程池,必须对其原理了如指掌。

    线程的几种状态

    线程在一定条件下,状态会发生变化。根据 线程的几种状态 这篇文章,线程一共有以下几种状态:

    1、新建状态(New) :新创建了一个线程对象。
    2、就绪状态(Runnable) :线程对象创建后,其他线程调用了该对象的 start() 方法。该状态的线程位于“可运行线程池”中,变得可运行,只等待获取CPU的使用权,即 在就绪状态的线程除CPU之外,其它的运行所需资源都已全部获得。
    3、运行状态(Running) :就绪状态的线程获取了CPU,执行程序代码。
    4、阻塞状态(Blocked) :阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

  • ①. 等待阻塞:运行的线程执行 wait() 方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的必须依靠其他线程调用 notify() notifyAll() 方法才能被唤醒。
  • ②. 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
  • ③. 其他阻塞:运行的线程执行 sleep() join() 方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当 sleep() 状态超时、 join() 等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。
  • 5、死亡状态(Dead) :线程执行完了或者因异常退出了 run() 方法,该线程结束生命周期。

    线程变化的状态转换图如下:

    拿到对象的锁标记,即为获得了对该对象(临界区)的使用权限。即该线程获得了运行所需的资源,进入“就绪状态”,只需获得CPU,就可以运行。

    因为当调用wait()后,线程会释放掉它所占有的“锁标志”,所以线程只有在此获取资源才能进入就绪状态。

    下面作下解释:

  • 线程的实现有两种方式,一是继承Thread类,二是实现Runnable接口,但不管怎样, 当我们new了这个对象后,线程就进入了初始状态;
  • 当该对象调用了start()方法,就进入就绪状态;
  • 进入就绪后,当该对象被操作系统选中,获得CPU时间片就会进入运行状态;
  • 进入运行状态后情况就比较复杂;
    1. run() 方法或 start() 方法结束后,线程就进入终止状态;
    2. 当线程调用了自身的 sleep() 方法或其他线程的 join() 方法,进程让出CPU,然后就会进入阻塞状态(该状态既停止当前线程,但并不释放所占有的资源,即调用 sleep() 函数后,线程不会释放它的“锁标志”。)。当 sleep() 结束或 join() 结束后,该线程进入可运行状态,继续等待OS分配CPU时间片;典型地, sleep() 被用在等待某个资源就绪的情形;测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。
    3. 线程调用了 yield() 方法,意思是放弃当前获得的CPU时间片,回到就绪状态,这时与其他进程处于同等竞争状态,OS有可能会接着又让这个进程进入运行状态;调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间片从而需要转到另一个线程。 yield() 只是使当前线程重新回到可执行状态,所以执行 yield() 的线程有可能在进入到可执行状态后马上又被执行。
    4. 当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被synchronized(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记(这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于队列状态,既先到先得),一旦线程获得锁标记后,就转入就绪状态,等待OS分配CPU时间片。
    5. suspend() resume() 方法:两个方法配套使用, suspend() 使得线程进入阻塞状态,并且不会自动恢复,必须其对应的 resume() 被调用,才能使得线程重新进入可执行状态。典型地, suspend() resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
    6. wait() notify() 方法:当线程调用 wait() 方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用 notify() notifyAll() 方法才能被唤醒(由于 notify() 只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒, 因此在实际使用时,一般都用 notifyAll() 方法,唤醒有所线程),线程被唤醒后会进入锁池,等待获取锁标记。 wait() 使得线程进入阻塞状态,它有两种形式: 一种允许指定以ms为单位的时间作为参数,另一种没有参数。前者当对应的 notify() 被调用或超出指定时间时线程重新进入可执行状态即就绪状态,后者则必须对应的 notify() 被调用。 当调用 wait() 后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用。 wait() notify() 因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronized block中进行调用。 如果在non-synchronized函数或non-synchronizedblock中进行调用,虽然能编译通过,但在运行时会发生 IllegalMonitorStateException 的异常。
    7. 线程池ThreadPoolExecutor实现原理

      我们先看下ThreadPoolExecutor的继承关系:

      1
      2
      3
      4
      public class ThreadPoolExecutor extends AbstractExecutorService { ... }
      public abstract class AbstractExecutorService implements ExecutorService { ... }
      public interface ExecutorService extends Executor { ... }
      public interface Executor { ... }

      再看看ThreadPoolExecutor的构造方法了解一下这个类:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      public ThreadPoolExecutor(int corePoolSize,
      int maximumPoolSize,
      long keepAliveTime,
      TimeUnit unit,
      BlockingQueue<Runnable> workQueue,
      ThreadFactory threadFactory,
      RejectedExecutionHandler handler) {
      //...省略...
      }

      构造参数比较多,一个一个说下:

    8. corePoolSize :线程池中的核心线程数;
    9. maximumPoolSize :线程池最大线程数,它表示在线程池中最多能创建多少个线程;
    10. keepAliveTime :线程池中 非核心线程闲置超时时长 (准确来说应该是没有任务执行时的回收时间,后面会分析);
  •