Java线程池初步理解

前言:在慕课网上学习剑指Java面试-Offer直通车时所做的笔记.

目录

第一章 基本概念

1.1 Fork/Join框架

1.2 为什么要使用线程池

第二章 源码理解

2.1 J.U.C的三个Executor接口

第三章 线程池的设计与实现

3.1 线程池的构造函数

3.2 线程池的执行步骤

3.3 线程池的状态

3.4 生命周期与线程池大小选定 


第一章 基本概念

在web开发中,服务器需要接受并处理请求,所以会为一个请求分配一个线程进行处理,如果并发的请求数量非常多,但每个线程执行的时间很短,这样就会频繁的创建和销毁线程,如此一来,会大大降低系统的效率,可能会出现服务器在为每个请求创建新线程和销毁线程上花费的时间和消耗的系统资源要比处理实际的用户请求的时间和资源更多,我们需要一种方法能够重复的利用线程去完成新的任务.

一般利用Executors创建不同的线程池满足不同的场景需求.

位于JUC包下的Executors目前提供了五种不同的线程池创建配置.

第五种方法是JDK8才引入的创建线程池的方法,我们下面大概讲解一下ForkJoinPool

1.1 Fork/Join框架

此框架是java7提供的并行执行任务的框架.

总的来说是一个:

Fork/Join框架是ExecutorService接口的一种具体的实现,目的是为了更好地利用多处理器带来的好处,它是为那些能递归的拆分成子任务的工作类型量身设计的,其目的在于能够使用所有可用的运算能力来提升你的应用的性能,这点与mapreduce是一样的,Fork/Join会将任务分发给线程池中的工作线程,它使用Work-Stealing算法.

Fork/Join将子任务放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,那么这里会出现一种情况,有些线程任务队列的任务已经完成,有的队列还有任务没有完成,这就造成已完成任务线程会被闲置,为了提高效率,完成自己任务而处于空闲的线程能够从其它仍处于busy状态的工作线程处窃取等待执行的任务.为了减少窃取任务线程和被窃取任务线程间的竞争,通常会使用双端队列,被窃取任务线程永远会从双端队列的头部执行,而窃取任务的线程永远从双端队列的尾部执行.

Work-Stealing算法:某个线程从其他队列里窃取任务来执行.

 

1.2 为什么要使用线程池

1.降低资源消耗,通过重复利用已创建的线程来降低线程创建和销毁造成的消耗

2.提高线程的可管理性,线程是稀缺资源,重复创建增大系统的消耗与不稳定性.使用线程池可以进行统一的分配,调优和监控.

 

第二章 源码理解

找到juc包中,发现几个新建线程池的方法都会返回ThreadPoolExecutor,只是它们的参数是不一样的.

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


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


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

点进ThreadPoolExecutor的源码之中,发现其继承链如下.

对于newSingleThreadScheduledExecutor,其返回是DelegatedScheduledExecutorService,

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }

发现其继承链仍包含ExecutorService.

所以这四类线程都起源于Executor,

查看ForkJoinPool的源码,发现其本质上也实现了ExecutorService接口.

public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

总的来说,它们都属于Executor框架体系下的类或者接口.

Executor框架是一个根据一组执行策略调用调度执行和控制的异步任务的框架,目的是提供一种将任务提交与任务如何运行分离开来的机制.

2.1 J.U.C的三个Executor接口

Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦.

可以看到Executor中只有一个方法,对于不同Executor的实现,Executor方法可能是创建一个新线程并立即启动,也可能是使用已有的工作线程来运行传入的任务,也可能是根据设置线程池的容量或阻塞队列的容量来决定是否要将传入的线程放入阻塞队列中或拒绝接收传入的线程.

public interface Executor {
    void execute(Runnable command);
}

具体执行:

ExecutorService:扩展了Executor接口,添加了一些管理执行器生命周期和任务生命周期的方法,提供了更全面的提交任务机制.如返回future和无视void的submit方法.

回到源码中,打开ExecutorService

可以看到其传入的参数类型是Callable,Callable弥补了Ruuable无法返回结果的短板,submit较executor提供了更加完善的提交任务机制.

<T> Future<T> submit(Callable<T> task);

ScheduledExecutorService扩展了ExecutorService,同时支持Future和定期执行任务.

总的来说:java提供了上述三个接口的标准库实现,如ThreadPoolExecutor,ScheduledThreadPoolExecutor,这些线程池的设计特点在于其高度的可调节性和灵活性,以尽量满足复杂多变的实际应用场景,Excecutors则从简化使用的角度为我们提供了各种方便的静态工厂的方法.

 

第三章 线程池的设计与实现

在大多数应用场景下,使用Executors提供的五类线程池就足够了,但还是有些场景需要直接利用ThreadPoolExecutor等构造函数去创建.这就要求对线程构造方法有进一步的了解.

我们这里对最重要的ThreadPoolExecutor进行分析.

应用提交任务到线程池去处理,然后再到线程池内部如何处理任务,再到处理完成,返回给应用的流程,可以看到线程池有一个工作队列来接客,即存储用户提交的各个任务,队列可以是SynchronousQueue<Runnable>,也可以是LinkedBlockingQueue,队列接客完毕后,就会把任务提交给内部的线程池即工作线程的集合,该集合需要在运行的过程中管理线程的创建和销毁,线程池的工作线程被抽象为静态内部类Worker,ThreadPool维护的就是Worker对象.

worker对象中的firstTask用它来保存传入的任务,是在调用构造函数方法时通过ThreadFactory来创建出来的线程,因为worker实现了Runnable接口也就是一个线程了,所以worker启动的时候会调用worker里的run方法去执行里面的逻辑.

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
        {
        //...
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
        /** Per-thread task counter */
        volatile long completedTasks;
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
        public void run() {
            runWorker(this);
        }
        //...
}

在源码中我们看到,ThreadFactory提供线程池所需的创建线程的逻辑,如果任务提交被拒绝,比如线程池已经处于关闭的状态,此时新来的任务需要有处理机制来处理,Java标准库里面就提供了一些实现了RejectExecutionHandler接口的类供我们使用.

3.1 线程池的构造函数

构造线程池时有一些初始化参数如nthreads,threadFactory.

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

各个初始化参数的含义: 

默认使用的是dafaultThreadFactory,用其创建出来的线程会有相同的priority(优先级)并且是非守护线程,同时也设置了线程的名称.

 handler参数:

3.2 线程池的执行步骤

新任务提交executae执行后执行的判断

 

线程池与线程一样也会有生命周期,生命周期也是通过状态值来表示的,此外因为线程池的作用就是用来管理线程的,那么对线程的数量其肯定也是清楚的,也就是说会有个地方存储当前有效的线程数,而ThreadPoolExecutor则将状态值和有效线程数合二为一存储到了ctl中,ctl是对线程池的运行状态和线程池中有效线程数量进行控制的一个字段,它主要包括两部分的信息,线程池的运行状态,线程池内有效线程的数量.ctl的高三位是用来保存runstate的,另外的29位是用来保存workcount的.

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;

状态的获取:

runStateOf是用来获取运行状态的,workerCountOf主要是用来获取活动线程数的,ctlOf是获取两者的,里面用的都是与或非的操作,执行起来是相当高效且优雅的.

3.3 线程池的状态

线程池有五种状态

状态转换过程:

3.4 生命周期与线程池大小选定 

工作线程的生命周期:

线程池大小的选定:

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章