Java juc系列6 —— 线程池

Java JUC系列目录链接

Java线程池的基础用法

在深入了解Java线程池之前,我们先来回顾一下线程池的基础用法。

创建和使用

想要创建一个线程池有两种方法(其实底层都用的同一种):

  • 使用excutors的静态方法创建,比如
    ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
  • 使用ThreadPoolExecutor的构造方法,比如
    ThreadPoolExecutor singleThreadPool = new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>());

在使用线程池时用线程池的execute方法:

 singleThreadPool.execute(() -> {
            System.out.println("thread start");
        });

为什么需要线程池

好好的线程多简单直接new Thread简单粗暴,为什么要引入线程池这玩意儿,搞起来还这么麻烦?
在高并发下这个当然是很有必要的(当然你说你基本不会有高并发,直接new Thread当然可以,还可以简化代码),在解释原因之前我们先看几个理论的东西,理论不能少啊哈哈:

线程的生命周期1

这个生命周期很难找到很底层的原理,只在一个技术群处得到一位大佬的分享:

  • New(新建):线程在编程语言层面被创建,JVM分配内存并初始化成员变量,不允许分配CPU执行
  • Runable(就绪):线程对象调用start方法之后,线程处于就绪状态,jvm会为其创建方法调用栈和程序计数器,等待分配CPU执行
  • Running(运行状态):处于就绪状态的线程获得了CPU,开始执行run方法里的方法体,线程处于运行状态
  • 休眠:处于运行状态的线程失去所占用资源后,进入休眠状态。包含三种状态阻塞/有限时等待/无限时等待
  • Terminated(线程终止):线程生命终止
  • 唤醒:线程从休眠状态回到运行状态,重新获得CPU
新建

我们在Java中创建线程时,使用

new Thread();

调用Thread的构造方法,最终会执行到Thread的init方法:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
       // ......太长了就不贴了,主要内容与主题无关
    }

在该方法中jvm为该线程对象分配内存,此时时不允许线程执行的。

就绪

创建完成线程对象后,我们就可以调用

  thread.start();

来执行线程了:
最终会执行native方法start0:

private native void start0();

这个会在jvm层调用本地方法,如果想看源码的可以自选下载open-jdk查看,这里就不翻源码出来看了。
这个时候线程就会处于就绪状态,jvm会给这个线程分配线程栈和程序计数器,等待CPU进行调用。

运行

当获得CPU时间片后,线程就会进入运行状态。(什么时候获得CPU时间片这取决于操作系统的分配算法,与jvm没啥关系)

休眠

当线程因为某种原因而等待资源时(最常见的等待IO)时,线程会进入休眠状态,退出CPU分片,进入等待阻塞状态。

终止

当线程执行完所有操作或因为错误结束时,线程就会终止,退出CPU分片。

使用线程的代价

我们来梳理一下上面说的线程的生命周期,大概算一下使用线程的代价:

  • 新建线程jvm分配内存,这个在jvm层面,代价微乎其微不予考虑;
  • 线程进入就绪态会分配相应的线程栈并进入CPU等待队列,此时会进行CPU分片,这时候jvm需要用native方法进行系统调用,是存在一定的代价的
  • 线程运行和阻塞都是CPU自身层面的基本没有代价
  • 线程唤醒会重新获得CPU分片,此时存在一定代价
  • 线程结束终止时会释放CPU的时间片,并交与jvm释放线程的内存,有一定代价

一般的:涉及到到jvm应用程序(用户态)执行native系统调用(转入内核态)时,都会存在较高的代价。

线程池帮我们做什么

既然使用线程是存在代价的,那么我们为什么还要用线程呢?

  • 我们当然不用每次都用线程。假设我现在有一个任务,他需要花费T1时间来执行,我创建一个线程的代价需要T2时间,结束一个线程的代价需要T3时间。如果T2+T3 > T1。那么这种情况下是完全没有必要使用线程的。

此时线程池就应运而生了。线程池会在创建的时候初始化并为许多线程分配空间,而不需要每次使用线程的时候去创建,在使用完成后也可以回收后继续调用。创建线程步骤中代价较大是进入CPU等待队列并分配时间片,如果线程池能够降低代价的话,那么大胆的推测就来了:线程池中创建的核心线程是一直在进行自旋的!

线程池原理

多的不说,直接上源码,别忘了我们上面的大胆的推测。

创建线程池

先来看一下线程池的一些常量。

 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));// 1110-0000-0000-0000-0000-0000-0000-0000
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;// 0001-1111-1111-1111-1111-1111-1111-1111

    private static final int RUNNING    = -1 << COUNT_BITS;// 1110-0000-0000-0000-0000-0000-0000-0000 线程池正常运行
    private static final int SHUTDOWN   =  0 << COUNT_BITS;// 0000-0000-0000-0000-0000-0000-0000-0000 停止接受新任务
    private static final int STOP       =  1 << COUNT_BITS;// 0010-0000-0000-0000-0000-0000-0000-0000 中断所有任务并停止接收新任务
    private static final int TIDYING    =  2 << COUNT_BITS; //0100-0000-0000-0000-0000-0000-0000-0000
    private static final int TERMINATED =  3 << COUNT_BITS;// 0110-0000-0000-0000-0000-0000-0000-0000

    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    private static int ctlOf(int rs, int wc) { return rs | wc; }

    private static boolean isRunning(int c) {
        return c < SHUTDOWN;
    }

当我们new出线程池时,会执行线程池的构造方法:

        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;

进行入参效验和赋值,总的来说跟创建一个普通对象没什么区别。

  • 这里的acc是获取了权限管理,来确保后面进入线程池的任务是否有权限执行。

使用线程池excute

在创建完线程池后我们就可以使用它来执行我们的线程任务。调用threadpool.execute(command)就会进入到下面的方法:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        // ****1****
        if (workerCountOf(c) < corePoolSize) {
        // ****2****
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // ****3****
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
        // ****4****
            if (! isRunning(recheck) && remove(command))
                reject(command);
        // ****5****
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // ****6****
        else if (!addWorker(command, false))
            reject(command);
    }

如果你英语水平还行的话直接看jdk的注释就大概能懂什么意思了。这里我简单说说上面加标签的地方的含义

  • 1.判断当前正在执行的work数量,如果小于初始化时的最大核心线程数,说明当前存在空闲核心线程;

  • 2.直接将command加入到核心线程中去执行,如果加入成功后面的就不用看了;

  • 3.当前没有空闲的核心线程,并且当前的线程池处于活动状态,则尝试将这个command加入到等待核心线程的队列中去;

  • 4.这时候进行了一步recheck的操作,用来确保当前线程池的ctl状态可以执行任务。那么,什么操作会改变ctl呢?

     private boolean compareAndIncrementWorkerCount(int expect) {
            return ctl.compareAndSet(expect, expect + 1);
        }
    
     private boolean compareAndDecrementWorkerCount(int expect) {
          return ctl.compareAndSet(expect, expect - 1);
        }

    ThreadPoolExecutor中只有这两个方法使用CAS方法更新ctl的值,那么我们再看何时会调用这些方法

    • 在addWorker(添加任务)中调用了compareAndIncrementWorkerCount增加ctl的值;
    • 在getTask中调用了compareAndDecrementWorkerCount减小ctl的值;
    • 在addWorkerFailed(添加任务失败)和processWorkerExit(任务因异常退出)中间接调用了compareAndDecrementWorkerCount减小ctl的值。

    使用类似双重检查的逻辑来保证ctl的状态正确(严格意义上这个是不能绝对保证的,见双重检查)

  • 5.如果当前没有活跃的核心线程,则使用addWorker来添加核心线程。

  • 6.将当前的command加入到即将运行的队列中,如果加入失败(线程池超出等待队列上限或是一般异常),则拒绝此次任务。

调用的addWorkder用于创建核心线程或是将任务加入到核心线程/核心线程等待队列中去。
我们的线程池有三个位置来存放command:

  • 当前正在执行的command由核心线程执行。
  • 等待队列存放等待队列中的command。
  • 剩余任务存放在等待入队的集合中。
    线程池execute流程图
    图片转自博客园

线程池模型

这里就直接转载知乎的一片文章了。
线程池模型-转自知乎

最后再说一句,看源码不要太纠结,不需要每一行都看懂,能够明白思路就可以了。


  1. (来自某群大佬的分享,不是原创) ↩︎

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值