关于 Java 线程与线程池的那些事

目录

线程如何使用?

通过 Runnable 来实例化 Thread 实例有什么不同吗?

线程池又是什么鬼?

ThreadPoolExecutor 的原理剖析

ThreadPoolExecutor 工作的流程图         ​

 拒绝策略

 如何维护线程池的状态?


线程如何使用?

        Java 中常用的实现线程的方式为继承 Thead 或者实现 Runnable 接口,废话不多说,直接上代码

public class ThreadRunnableTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new MyThread();
        thread.start();

        Thread thread2 = new Thread(new MyRunnable());
        thread2.start();

        Thread.sleep(1000);
        MyRunnable myRunnable = new MyRunnable();
        myRunnable.run();
        System.out.println(Thread.currentThread() + "========Main=======");
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread() + "==========MyThread========");
    }
}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread() + "=========MyRunnable=======");
    }
}

// 执行结果
Thread[Thread-0,5,main]==========MyThread========
Thread[Thread-1,5,main]=========MyRunnable=======
Thread[main,5,main]=========MyRunnable=======
Thread[main,5,main]========Main=======

        可以看到 Runnable 实例可以自己实例化调用自己的 run 方法来执行,但是这样的话它就只是一个普通的 Java 对象,并没有创建一个线程出来,从打印日志也可以看出来是主线程的信息。Runnable 实例也可以作为一个参数传给 Thread 时才会创建一个线程。

通过 Runnable 来实例化 Thread 实例有什么不同吗?

public class ThreadRunnableTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new MyThread();
        Thread thread2 = new MyThread();

        thread1.start();
        thread2.start();
    }
}

class MyThread extends Thread {
    int k = 0;
    @Override
    public void run() {
        while (k < 10) {
            System.out.println(Thread.currentThread() + "" + (++k));
        }
    }
}

class MyRunnable implements Runnable {
    int k = 0;
    @Override
    public void run() {
        while (k < 10) {
            System.out.println(Thread.currentThread() + "" + (++k));
        }
    }
}

//执行结果
Thread[Thread-1,5,main]1
Thread[Thread-0,5,main]1
Thread[Thread-1,5,main]2
Thread[Thread-0,5,main]2
Thread[Thread-1,5,main]3
Thread[Thread-0,5,main]3
Thread[Thread-1,5,main]4
Thread[Thread-0,5,main]4
Thread[Thread-1,5,main]5
Thread[Thread-1,5,main]6
Thread[Thread-0,5,main]5
Thread[Thread-1,5,main]7
Thread[Thread-0,5,main]6
Thread[Thread-1,5,main]8
Thread[Thread-0,5,main]7
Thread[Thread-1,5,main]9
Thread[Thread-0,5,main]8
Thread[Thread-1,5,main]10
Thread[Thread-0,5,main]9
Thread[Thread-0,5,main]10

        从上面的样例可以看到,该程序是创建了两个线程,然后两个线程之间是资源不共享的,各干各的事,接着我们再看下下面的例子

public class ThreadRunnableTest {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new MyRunnable();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();
    }
}

class MyRunnable implements Runnable {
    int k = 0;
    @Override
    public void run() {
        while (k < 10) {
            System.out.println(Thread.currentThread() + "" + (++k));
        }
    }
}

//执行结果
Thread[Thread-0,5,main]1
Thread[Thread-1,5,main]2
Thread[Thread-0,5,main]3
Thread[Thread-1,5,main]4
Thread[Thread-0,5,main]5
Thread[Thread-1,5,main]6
Thread[Thread-0,5,main]7
Thread[Thread-1,5,main]8
Thread[Thread-0,5,main]9
Thread[Thread-1,5,main]10

        可以看到,该程序也是创建了两个线程,但是他们都是由同一个 Runnable 实例创建出来的,所以可以共享了 k 这个资源,可以使用该机制让多个线程干同一件事情,但是如果不使用 Runnable 作为参数来构造 Thread 实例的话,各个线程就只能干自己的事情。

线程池又是什么鬼?

        简单来讲,线程池就是缓存线程的一个东西,比如说我可以自己写个类,然后把 Thread 实例 保存起来,比如说用链表或者数组,反正能保存起来,然后每次用线程的时候直接拿就好了,不用每次都 new 一个线程出来了。

        按照上面说的,线程池的主要作用好像就是缓存一下线程实例,那么创建线程池有什么好处呢,首先需要明白的一点是,Java 层级是无法创建一个线程的,所谓的 Java 线程只不过是通过调用底层操作系统提供的接口来创建了一个线程出来,这个过程中又设计态用户态到内核态的来回切换,所以频繁的创建和销毁线程是很消耗系统资源的。而如果请求只是一个简单逻辑的请求,那么处理请求的可能比处理线程的时间还要长,所以在这个前提下,线程池的存在还是很有必要的。

ThreadPoolExecutor 的原理剖析

        ThreadPoolExecutor 是 Java 中常用到的线程池,所以这里以它为例分析一下线程池的实现原理,首先举个例子

         从上图可以看出,每个公司都会承接一些项目,但是要正常完成客户的需求,需要满足以下条件。

        首先第一步就是要雇佣一些打工仔来干活。但是按照公司的经验,一般不会有很多客户,所以公司人员的数量基本也就那么多,不会浮动太大,此为常驻人员。

        有的时候客户突然变多了,只需要将对应的需求先放着,等公司的打工仔把手头工作做完了再去认领新的工作,这也就是第二步对应的情况。

        但是有的时候客户实在是着急呀,没办法等打工仔忙完,这个时候就需要紧急扩充人员,就有了第三步,那么什么是总共可扩充的人员呢,可以理解为每个公司招聘都是有预算的,不能无限制的疯狂招人,所以就有了这个限制。

        既然公司人数是有最大值的,那么当扩招都招满了的时候,还是没法满足客户的需求应该怎么办呢?此时就需要进行第四步的特殊处理操作,这个特殊处理方式有很多,比如你可以拒绝这个客户的需求,说不伺候您了。

        此外,因为公司需要的常驻人数一般也就那么多个,所以还需要有个解雇员工的机制,对于临时扩招的情况,等忙完这段之后,一定会有些打工仔比较闲,如果打工仔摸鱼的时间超过了一定时间,就需要将其解雇。

        再回到代码上来,看下该线程池的构造函数

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

    //名次解释:
    ThreadPoolExecutor: 线程池对象,即对应举例中的公司
    corePoolSize: 核心线程数,即对应举例的常驻人员
    maximumPoolSize: 最多可以拥有的线程数, 对应最多可以有多少打工仔
    keepAliveTime: 线程可以不工作的最长时间,对上上面打工仔最长的摸鱼时间
    unit: 时间的单位,可以指定时,分,秒 等等
    workQueue: 阻塞队列,暂存线程信息。对应举例的任务队列

    可以看到该构造函数还调用了另外一个构造方法,还多了两个参数

    ThreadFactory:线程工程,用来生产线程,可以理解为培训机构培训打工仔
    RejectedExecutionHandler: 拒绝策略,可以理解为上面举例特殊处理的情况
    

       上面只是出现了两个数量限制 corePoolSize,maximumPoolSize 那么此时你可能会有疑问,打工仔呢,打工仔到底是线程还是什么玩意,怎么还没露头?先不着急,看下代码怎么用的

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
            100, MILLISECONDS, new LinkedBlockingDeque<>());

        executor.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("哈哈哈");
            }
        });
        executor.shutdown();
    }
}

// 打印结果:
哈哈哈

        使用其实就是这么简单,下面我们看下 submit 方法到底干了啥

    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    // 继续跟进 execute 方法
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            // 当小于 corePoolSize 开始雇佣打工仔
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 常驻已满,先把任务放到队列里
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // 扩招员工
        else if (!addWorker(command, false))
            // 扩招员工失败,触发特殊处理操作
            reject(command);
    }

        从上面的 execute 方法可以看出来上面的几个 if 就对应我们原理图的四个步骤的操作, 接下来我们看下 addWorker 方法看下打工仔worker到底是个啥吧

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                // 判断是否还可以创建 worker 对象,即雇佣打工仔
                // core 为 true 代表在创建核心线程,为 false 的时候代表创建扩充的线程,所以需根据它来判断跟 corePoolSize maximumPoolSize 中哪一个进行比较
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                // 可以继续雇佣打工仔的话,就跳出这一系列判断逻辑,走下面的创建 worker 的逻辑
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //创建一个 Worker 对象,这就是打工仔哦
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());
                    // 这个主要是判断线程池状态,看下线程池还能不能接受新的打工仔
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        // 将打工仔保存起来,它就是个 HashSet
                        workers.add(w);
                        int s = workers.size();
                        // 这一步是为了记录线程池中拥有过的最多打工仔的记录
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    // 启动线程,让打工仔开始干活
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

        从上面可以看到核心逻辑有两个,一是 w = new Worker(firstTask); 通过它我们可以知道打工仔到底是个啥。第二就是 t.start() 由此可以知道打工仔是怎么工作的。

这是 ThreadPoolExecutor 的内部类
private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable {

        //....省略部分代码
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            // firstTask 是 submit 时传递进来的 Runnable 实例
            this.firstTask = firstTask;

            // 给该 Worker 创建一个线程,注意这里传递的是 this, 也就是 Work 对象,从上面也可以看出来,Worker 实现了 Runnable 接口,所以当执行 thread.start 的时候,调用的是 worker 的 run 方法
            this.thread = getThreadFactory().newThread(this);
        }

        // 重写 run 方法
        public void run() {
            runWorker(this);
        }

        //...省略部分代码
}


// 打工仔真正干活的方法,注意这是 ThreadPoolExecutor 的函数,而不是 Worker 内部的哦
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    // 取出 task 之后,就将其置为空,用于标识该task已经被处理过
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        //当 task 为空时,需要调用 getTask 从阻塞队列里获取一个任务,如果不为空,说明需要执行当前 worker 的任务
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 这里才真正执行最外层线程池提交的 Runnable 实例的 run 方法
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

        到了这里基本就已经很清楚了,只剩 getTask 还没看了,虽然我们已经猜出来他就是去阻塞队列里取任务,但是出于尊重,还是看一看吧

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // 这里是判断是否有待解雇的打工仔,注意这里有两个机制,下面分析。
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            // 打工仔个数 - 1
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 从 workQueue 中获取任务
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

        这里有几点需要注意,在判断是否需要解雇打工仔的时候,有两个机制,一是上面分析的临时扩招导致打工仔数量超过 corePoolSize 了,如果有的打工仔摸鱼时间超过 keepAliveTime 了,说明不需要那么多打工仔了,就可以将其炒鱿鱼。二是配置 allowCoreThreadTimeOut = true, 这表明公司当没有需求的时候,一个打工仔都不留,不允许任何一个人摸鱼超过 keepAliveTime 的时间,但是往往不会这么搞,所以默认为 false。此外还看到上面只是将打工仔个数 - 1,那么具体是怎么销毁的呢?如果有印象的话,上面已经将 worker.firstTask 置为空了,代表已经没有别的对象对其的引用。所以在 GC 的时候就将其回收了。最后需要注意的是,并没有给打工仔标记 corePoolSize 还是 maximumPoolSize,这两个数字只是逻辑上的一个限制,并没有真正想上面原理图里那样用两个容器分别管理打工仔,所以销毁的时候并不会区分,销毁的可能是最先招聘的打工仔,也可能是最后招聘的打工仔。

ThreadPoolExecutor 工作的流程图         

看不清的话可以点链接过来看https://www.processon.com/view/link/61f9edca0e3e7407d4beffa3

 拒绝策略

        当阻塞队列已经满了,且没有工作线程可以使用的时候,如果此时还有任务提交,则必须采用一种拒绝策略来处理该请求,线程池提供了 4 中拒绝策略

  • AbortPolicy: 直接跑出异常,默认的策略
  • CallerRunsPolicy:用调用者所在的线程来执行任务
  • DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
  • DiscardPolicy:直接丢弃任务

        实际应用中可以结合具体场景来决定使用哪种拒绝策略,当然也可以自己写适合自己场景的拒绝策略。这里介绍一下 AbortPolicy 和 CallerRunsPolicy    

    

public static class AbortPolicy implements RejectedExecutionHandler {
    /**
     * Creates an {@code AbortPolicy}.
     */
    public AbortPolicy() { }

    /**
     * Always throws RejectedExecutionException.
     *
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     * @throws RejectedExecutionException always
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException("Task " + r.toString() +
                                             " rejected from " +
                                             e.toString());
    }
}

//可以看到该策略直接抛出了异常
public static class CallerRunsPolicy implements RejectedExecutionHandler {
    /**
     * Creates a {@code CallerRunsPolicy}.
     */
    public CallerRunsPolicy() { }

    /**
     * Executes task r in the caller's thread, unless the executor
     * has been shut down, in which case the task is discarded.
     *
     * @param r the runnable task requested to be executed
     * @param e the executor attempting to execute this task
     */
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run();
        }
    }
}

// 可以看到这里直接调用了 r.run(),此时需要注意的是,这个时候并不是线程池中任何一个 worker 来执行的,而是接受该 Runnable 的线程执行的。按照本例的调用方式,因为是在主线程调用了 submit 方法,所以此时是由主线程来处理的该 Runnable, 就跟在 main 方法里 Runnable r = new Runnable(); r.run() 一样。

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10,
            100, MILLISECONDS, new LinkedBlockingDeque<>());

        executor.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("哈哈哈");
            }
        });
        executor.shutdown();
    }
}

 如何维护线程池的状态?

        上面把主线流程讲完了,但是还有一点很重要的需要补充,想看大家在看代码的过程中也注意到了有些获取线程池状态或者获取 Worker 数量的方法,比如 ctl.get()、 runStateOf(c)、workerCountOf(c) 等等,这些方法都依赖于一个很重要的变量  ctl。与 ctl 相关的属性和方法如下所示。

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;

// Packing and unpacking ctl
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; }

         ctl 是对线程池的运行状态和线程池中有效线程数量进行控制的一个字段,线程池的状态有 RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。通过 runStateOf(ctl) 来获取当前线程池的状态。通过 workerCountOf(ctl) 来获取 Worker 的数量。

        ctl 是 Integer 类型,我们知道 Integer 有 32 位,这里使用高 3 位保存保存线程池的运行状态,低 29 位保存 Worker 的数量,如上所示,COUNT_BITS = 29, CAPACITY = (1 << 29) - 1, 不过这个 CAPACITY 代表的是线程池所能拥有的最大的 Worker 的数量,即最多有 5 亿+,而不是实际就有那么多。

        我们分析一下 runStateOf 方法,我们知道 CAPACITY 的二进制表示为 "00011111111111111111111111111111", 所以对其取反就是 "11100000000000000000000000000000"。ctl & ~CAPACITY 就代表着 ctl 和 "11100000000000000000000000000000" 进行与运算,因为后者低 29 位全为 0, 所以运算结果的后 29 为也一定为 0,因为后者前 3 位全部为 1,所以运算结果的前三位跟 ctl 的前三位是一致的。所以运算结果一定是 RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED 其中的一个,所以如果线程池运行状态发生了改变,只需要修改 ctl 的高三位中的就好了,最后通过 runStateOf 就可以获取到修改后的状态。

        workerCountOf 方法跟 runStateOf 原理一致,盆友们自己去分析一下吧~

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值