并发编程:线程池介绍

线程池

  • 使用线程池的好处
  • 线程池工作原理
  • 创建线程池
  • 线程池执行任务
  • 线程池生命周期管理
  • worker

JAVA 中的线程池是应用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在阿里巴巴《java 开发手册》中甚至说明线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。足以显示线程池的重要性。

使用线程池的好处

  1. 降低资源消耗

    通过重复利用已创建的线程降低线程创建和销毁造成的消耗。对操作系统来说,创建一个线程的代价是十分昂贵的, 需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。

  2. 提高响应速度

    任务到达时,任务可以不需要等待线程创建完毕。

  3. 提高线程的可管理性

    线程是稀缺资源。如果无限制的创建。不仅会消耗系统资源。还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

线程池工作原理

其运行机制如下图(图片来自文末美团技术文章)

在这里插入图片描述

使用者调用 execute、submit 提交任务到线程池。线程池有四步来处理这个任务

  1. 先看看能不能创建核心线程,即便有核心线程空闲,但是核心线程数没到核心线程数的最大值,还是选择新建核心线程来处理
  2. 如果核心线程数满了,就加入任务队列中。
  3. 队列也满了,但是线程总数(线程总数 >= 核心线程数)没有达到限制,就创建新的工作线程来处理任务
  4. 任务是在是太多了,线程数已经到达最大线程数了,执行饱和策略,如:丢弃

线程池execute源码如下

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {  // 1.创建核心线程
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) { // 2.核心线程满了,试试看能不能扔进队列里
            int recheck = ctl.get();										 // 扔进队列里再重复检查一下
            if (! isRunning(recheck) && remove(command)) // 线程池关闭了,那队列里的任务也执行不到了,直接拒绝任务 
                reject(command);
            else if (workerCountOf(recheck) == 0)        // 工作线程刚好全销毁了,起一个非核心线程把队列里的任务消费一下
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) // 3. 队列添加失败,试试看创建非核心线程  
            reject(command);	               // 4. 创建非核心线程失败,那就只能执行饱和策略了
    }

创建线程池

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
  1. corePoolSize :核心线程数量

  2. maximumPoolSize :所有线程的数量

  3. keepAliveTime 、 unit : 空闲线程的存活时间,当线程数 > corePoolSize 并且该线程空闲了 keepAliveTime 时间后,就会销毁,直到线程数 == corePoolSize

  4. BlockingQueue :任务阻塞队列,多个线程对队列进行操作时会阻塞,保证并发安全。

    常见的阻塞队列有ArrayBlockingQueue、LinkedBlockingQueue

    ArrayBlockingQueue、LinkedBlockingQueue 顾名思义一个是用数组结构,一个是链表结构,值得一提的是两者实现线程安全的原理都是使用了ReentrantLock,但是通常情况下 LinkedBlockingQueue 的吞吐量要更高,因为 ABQ 使用一个全局锁,而LBQ 是链表结构添加任务到队尾和拿队列头的任务是独立的两个操作,所以使用了两个锁,生产锁和消费锁。

    SynchronousQueue : 不存储元素的队列,每一个生成操作需要另一个线程调用消费操作,否则一直处于阻塞状态

    PriorityBlockingQueue:具有优先级的无限阻塞队列

  5. ThreadFactory :通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。

  6. RejectedExecutionHandler : 饱和策略,当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。AbortPolicy 为默认执行策略。

    • AbortPolicy:直接抛出异常,会 throws RejectedExecutionException
    • CallerRunsPolicy:调用者所在线程自己来运行任务
    • 其把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列
    • DiscardPolicy:不处理,丢弃掉

线程池的顶层接口和设计为Executor,里面只定义了一个execute 方法。

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

所有的线程池都实现这个接口,Executor 将任务的提交和执行分离开来,用户只需要通过execute 方法来提交任务,后续的工作由实现了Executor 类的各种线程池实现类来对任务进行执行。

ThreadPoolExecutor 就是Executor 的核心类,依据这个类,传入不同的参数,可以得到不同特性的线程池啦满足各种业务需求。甚至实现动态线程池。

JDK中也提供了3种不同特性的线程池:

  • FixedThreadPool : corePoolSize 和 MaximumPoolSize 都被设置为一个指定的参数,并且将线程空闲时间设置为0。队列设置为LBQ无界队列。线程池完成预热之后,线程数稳定,队列无限,处理任务速度基本不变。

  • SingleThreadPool 使用单个线程:corePoolSize 和 MaximumPoolSize 都被设置为1,其他都和FixedThreadPool 一样。

  • CachedThreadPool: corePoolSize 为0,MaximumPoolSize 为 integer 最大值。线程空闲存活时间为 60s。队列为无容量的 SynchronousQueue。如果主线程提交任务速度很快,那么 CachedThreadPool 会不断创建新线程。很有可能会耗尽CPU和内存资源。

    主线程提交任务时是直接提交给 SynchronousQueue 的,由于该队列会阻塞住主线程的提交,直到有线程执行poll来将该任务取走执行。如果线程池空着,那么会创建一个线程来拿。线程执行完之后,也会执行poll操作等待主线程提供任务。如果60s内都没有拿到任务,那么该线程就会被终止。

阿里巴巴《java开发手册》中不允许使用Executors 创建上述的模板线程池

Executors 创建的线程中使用了无界队列、integer 最大值的线程数量这些无限制的限制条件。

LinkedBlockingQueue阻塞队列。当核心线程用完后,任务会入队到阻塞队列,如果任务执行的时间比较长,没有释放,会导致越来越多的任务堆积到阻塞队列,最后导致机器的内存使用不停的飙升,造成JVM OOM。

不允许使用无界队列。

线程池执行任务

一共有两种方法提交任务:

  • execute( )
  • submit( )

两者区别:

  • execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。
  • execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法(阻塞) 获取返回值时,才会抛出异常。

多线程异步

Future的get方法是阻塞的,对于结果的获取,不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。Future提供了一个isDone方法,可以在程序中轮询这个方法查询执行结果。

阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源

因此,JDK8设计出CompletableFuture。CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。

CompletableFuture 可以传入自定义线程池,实现异步调用。如果不传入自定义线程池,也有默认的线程池。

CompletableFuture 创建异步任务的两个方法:

public static CompletableFuture<Void> runAsync(Runnable runnable,  Executor executor)  // 无返回值
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor) // 有返回值

线程池生命周期管理

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量ctl 维护两个值:运行状态(runState)和线程数量 (workerCount)。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
  1. ctl (线程池控制状态)是原子整型的,这意味这对它进行的操作具有原子性。
  2. 同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。同时具有了原子性。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。

ctl 使用了一系列位运算操作,将这两个状态打包为一个变量和分别拿出对应的值。如:

1 //拆包函数
2 private static int runStateOf(int c)     { return c & ~CAPACITY; }
3 private static int workerCountOf(int c)  { return c & CAPACITY; }
4 //打包函数
5 private static int ctlOf(int rs, int wc) { return rs | wc; }

在这里插入图片描述

线程池调用shutdown( ) 方法:将线程池状态变为 SHUTDOWN ,中断所有没有正在执行的线程,就是调用线程的interrupt方法来中断线程,但是这块仅仅是调用中断方法,将中断标志 interrupted 置为true 并不意味着线程就会终止,直到该线程执行的任务中有方法如:acquireInterruptibly 会响应中断。
在这里插入图片描述

线程池调用shutdownNow( ) 方法 : 将线程池状态变为 stop, 尝试终止所有正在执行的线程,和上面一样也只是遍历线程然后调用线程的interrupt方法。

线程生命周期转换如下图所示(图片来自文末美团技术文章)

在这里插入图片描述

worker

线程池将每一个线程和他拿到的任务包装为一个 Worker 内部类, 使用 completedTasks 记录执行完的任务数量。

 private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
        /** Per-thread task counter */
        volatile long completedTasks;
 }

 public void run() {
            runWorker(this);       
 }

 Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
  }

			protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }

worker 继承了AQS同步器,自己本身实现了锁的功能。自己使用了lock、tryLock()、unlock()、isLocked() 方法使用aqs实现锁的功能,相当于实现lock接口的方式。

将锁的粒度细化到每个工Worker,如果多个Worker使用同一个锁,那么一个Worker Running持有锁的时候,其他Worker就无法执行,这显然是不合理的。

什么时候将线程和任务封装为worker对象的?

用户使用线程池的时候就是执行execute或者submit方法将任务丢进去,那就要从execute 方法看起。execute方法里面的各种判断逻辑已经很清楚了,就是线程池工作原理。execute里面调用addWorker( ) 将任务对象继续往下传递。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);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                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 {
            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();
                        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;
    }

addWorker 源码总结一下就是:先自旋cas修改线程数量,修改成功了再去拿到线程池的全局锁,将新创建的worker 放入 Set 容器中。成功之后将worker线程开启。

注意worker 这里的 run 方法,调用了线程池的runWorker,并且将自己作为参数传进去,很容易想到runWorker 就是将worker里面的任务给执行了。先看看自己的任务有没有,再看看getTask()看看队列里有没有任务需要处理, 然后获取worker对象对应的锁,成功之后终于可以执行 task.run() 将任务执行掉。

需要注意的是这里设置了2个钩子函数:

  • beforeExecute(wt, task)
  • afterExecute(task, thrown)

这两个函数方法体为空,我们可以创建ThreadPoolExecutor的子类来重写beforeExecute(Thread, Runnable)方法,使得线程池正式执行任务之前,执行我们自己定义的业务逻辑。

processWorkerExit(Worker, boolean)方法的逻辑主要是执行退出Worker线程,并且对一些资源进行清理。

final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            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 {
                        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);
        }
    }

学习链接:

源码分析-使用newFixedThreadPool线程池导致的内存飙升问题

面试官:简述实现一个线程池的设计思路

Java线程池实现原理及其在美团业务中的实践

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值