Java并发编程——线程池详解

背景

在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多。

  • 经常创建和销毁线程,对性能的影响很大(上下文切换)

更好的解决方案是合理地利用线程池,线程池的优势很明显,如下:

  • 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  • 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
  • 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
  • 更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

Java中提供的线程池

Executors

  • newFixedThreadPool

    创建一个指定大小的线程池,可控制线程的最大并发数,超出的线程会在LinkedBlockingQueue阻塞队列中等待。

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

    创建一个单线程化的线程池,它只有一个线程,用仅有的一个线程来执行任务,保证所有的任务按照指定顺序(FIFO,LIFO,优先级)执行,所有的任务都保存在队列LinkedBlockingQueue中,等待唯一的单线程来执行任务。

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

    创建一个可缓存的无界线程池,如果线程池长度超过处理需要,可灵活回收空线程,若无可回收,则新建线程。当线程池中的线程空闲时间超过60s,则会自动回收该线程,当任务超过线程池的线程数则创建新的线程,线程池的大小上限为Integer.MAX_VALUE,可看作无限大。理论上来说,有多少请求,该线程池就可以创建多少的线程来处理。

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

    创建一个定长的线程池,可以指定线程池核心线程数,支持定时及周期性任务的执行。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
            
        return new ScheduledThreadPoolExecutor(corePoolSize); 
    }
    

ThreadPoolExecutor

通过观察以上线程池的构造方法,我们可以发现最后都是返回ThreadPoolExecutor对象,我们可以对其进行分析。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
     
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    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;
}

参数说明

  • corePoolSize(核心线程数):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,才会根据是否存在空闲线程,来决定是否需要创建新的线程。除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。
  • maximumPoolSize(最大线程数 ):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  • keepAliveTime(线程存活时间):默认情况下,当线程池的线程个数多于corePoolSize时,线程的空闲时间超过keepAliveTime则会终止。但只要keepAliveTime大于0,allowCoreThreadTimeOut(boolean) 方法也可将此超时策略应用于核心线程。另外,也可以使用setKeepAliveTime()动态地更改参数。
  • unit(存活时间的单位):时间单位,分为7类,从细到粗顺序:NANOSECONDS(纳秒),MICROSECONDS(微妙),MILLISECONDS(毫秒),SECONDS(秒),MINUTES(分),HOURS(小时),DAYS(天);
  • workQueue(阻塞队列):用于传输和保存等待执行任务的阻塞队列。可以使用此队列与线程池进行交互:
    • 如果运行的线程数少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
    • 如果运行的线程数等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
    • 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
  • threadFactory(线程工厂):用于创建新线程。由同一个threadFactory创建的线程,属于同一个ThreadGroup,创建的线程优先级都为Thread.NORM_PRIORITY,以及是非守护进程状态。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号);
  • handler(拒绝策略):当线程池和队列都满了,则表明该线程池已达饱和状态。
    • ThreadPoolExecutor.AbortPolicy:处理程序遭到拒绝,则直接抛出运行时异常RejectedExecutionException。(默认策略)
    • ThreadPoolExecutor.CallerRunsPolicy:调用者所在线程来运行该任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
    • ThreadPoolExecutor.DiscardPolicy:无法执行的任务将被删除。
    • ThreadPoolExecutor.DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重新尝试执行任务(如果再次失败,则重复此过程)。
    • 可以自定义拒绝策略(如果需要将任务存储起来,后续等空闲之后重试)

常用方法

异步执行

  • execute() 异步执行传递进来的任务

关闭线程池

  • shutdown()

线程池结束状态判断

  • isShutdown() 用来判断线程池是否已经关闭
    • 调用 shutdownNowshutdown 方法,isShutdown 会返回 true(即使还有任务正在运行)
  • isTerminated() 任务全部执行完毕,并且线程池已经关闭,才会返回 true

核心线程预启动

  • 默认情况下,核心线程只有在任务提交的时候才会创建
  • 预启动策略,可以让核心线程提前启动,从而增强最初提交的任务运行性能
  • 调用 ThreadPoolExecutor 类中的以下两个方法
    • prestartCoreThread() 启动1个核心线程
    • prestartAllCoreThreads() 启动所有核心线程

线程和线程池切面编程

  • 在线程执行前、执行后增加切面,在线程池关闭时执行某段程序
  • 需要实现自己的线程池类,并覆写 beforeExecuteafterExecuteterminated 方法

移除线程池中的任务

调用 ThreadPoolExecutor 类中的 remove() 方法

  • 已经正在运行中的任务不可以删除
  • execute方法提交的,未运行的任务可以删除
  • submit方法提交的,未运行任务不可以删除

获取线程池的各项数据

调用 ThreadPoolExecutor 类中的如下方法

  • 返回核心线程数 getCorePoolSize
  • 返回当前线程池中的线程数 getPoolSize
  • 返回最大允许的线程数 getMaximumPoolSize
  • 返回池中同时存在的最大线程数 getLargestPoolSize
  • 返回预定执行的任务总和 getTaskCount
  • 返回当前线程池已经完成的任务数 getCompletedTaskCount
  • 返回正在执行任务的线程的大致数目 getActiveCount
  • 返回线程池空闲时间 getKeepAliveTime

如何设计一个线程池?

如果让我们自己设计一个线程池,我们该如何做呢?

需求:实现线程的重复使用。

  • 如何实现线程的服用

    • 让线程实现服用的唯一方法,就是让线程不结束。

      • 那如何让线程执行新的任务呢?

        我们可以通过【共享内存】的方式来实现->比如:list.add(task);

      • 线程会一直处于运行状态吗?

        应该是有任务的时候,执行

        没有任务的时候,阻塞

    结论:我们可以通过阻塞队列的方式来实现线程池中线程的复用。

  • 线程池的实现原理的流程图:

    • 判断核心线程池是否已满,即已创建线程数是否小于corePoolSize?没满则创建一个新的工作线程来执行任务。已满则进入下个流程。
    • 判断工作队列是否已满?没满则将新提交的任务添加在工作队列,等待执行。已满则进入下个流程。
    • 判断整个线程池是否已满,即已创建线程数是否小于maximumPoolSize?没满则创建一个新的工作线程来执行任务,已满则交给饱和策略来处理这个任务。

    Java并发编程——线程池详解_第1张图片

源码分析

线程池中的核心线程是延迟初始化的。

  1. 先初始化核心线程。

  2. 调用阻塞队列的方法,把task存进去。(offer() -> true/false)

    1. 如果true ,说明当前的请求量不大, 核心线程就可以搞定。

    2. false,增加工作线程(非核心线程)

      如果添加失败,说明当前的工作线程数量达到了最大的线程数,直接调用拒绝策略

入口

public void execute(Runnable command) {
     
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    //判断当前工作线程数是否小于核心线程数(延迟初始化)
    if (workerCountOf(c) < corePoolSize) {
     
        //添加工作线程的同时,执行command
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //workQueue.offer 添加到阻塞队列
    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); //拒绝策略
}

添加

private boolean addWorker(Runnable firstTask, boolean core) {
     
    retry:
    //case1 通过原子操作来增加线程数量.
    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
        }
    }

    //case2 初始化工作线程.
    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
                        largestPoolSize = s;
                    workerAdded = true; //添加成功。
                }
            } finally {
     
                mainLock.unlock();
            }
            if (workerAdded) {
     
                t.start();  //启动线程
                workerStarted = true;
            }
        }
    } finally {
      //失败, 回滚。
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

private void addWorkerFailed(Worker w) {
     
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
     
        if (w != null)
            workers.remove(w);
        decrementWorkerCount();
        tryTerminate();
    } finally {
     
        mainLock.unlock();
    }
}

work.run()

t 是worker中的线程,我们可以看到worker类实现了Runable接口,所以t.start后触发了worker类中的run方法。

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable{
     }
public void run() {
     
    runWorker(this);
}
final void runWorker(Worker w) {
     
    // 获取当前线程对象的引用
    Thread wt = Thread.currentThread();
    // 获取worker的firstTask
    Runnable task = w.firstTask;
    // 获取完之后把worker的firstTask置为null 防止下次获取到
    w.firstTask = null;
    // 初始化worker的state = 0, exclusiveOwnerThread = null 解锁
    w.unlock(); // allow interrupts
     // 如果发生异常 当前线程突然退出 该值为true 
    boolean completedAbruptly = true;
    try {
     
        //while循环保证当前线程不结束. 直到task为null
        while (task != null || (task = getTask()) != null) {
     
            //表示当前线程正在运行一个任务,如果其他地方要shutdown().你必须要等我执行完成。
            w.lock();  //Worker继承了AQS -> 实现了互斥锁
            // 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();  //执行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);
    }
}
private Runnable getTask() {
     
    boolean timedOut = false; // Did the last poll() time out?

    //cas 自旋
    for (;;) {
     
        int c = ctl.get();
        int rs = runStateOf(c);

        // 如果线程池已经结束状态,直接返回null. 需要清理掉所有的工作线程
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
     
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        //是否允许超时 
        // * allowCoreThreadTimeOut 为true
        // * 如果当前的工作线程数量大于核心线程数
        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
     
            if (compareAndDecrementWorkerCount(c)) //cas 减少工作线程数量。
                return null; //表示要销毁当前工作线程.
            continue;
        }

        //获取任务的过程
        try {
     
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take(); //如果阻塞队列没有任务,当前工作线程会阻塞在这里
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
       //中断异常.
            timedOut = false;
        }
    }
}

合理配置线程池

需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。

  • 对于CPU密集型任务:线程池中线程个数应尽量少,不应大于CPU核心数;
  • 对于IO密集型任务:由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率;
  • 对于混合型任务:可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。

线程池监控

利用线程池提供的参数进行监控,参数如下:

  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
  • largestPoolSize:线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
  • getActiveCount:获取活动的线程数。

通过扩展线程池进行监控:继承线程池并重写线程池的beforeExecute(),afterExecute()和terminated()方法,可以在任务执行前、后和线程池关闭前自定义行为。如监控任务的平均执行时间,最大执行时间和最小执行时间等。

动态设置

我们也可以在运行过程中动态设置线程池的相关参数,比如核心线程数,最大线程数,阻塞队列长度等。

动态修改核心线程数

首先通过如下代码了解核心线程数的动态设置。代码中使用的是固定参数,实际操作中可以配合注册中心使用。

public class DynamicDemo {
     

    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 6, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>());

    public static void main(String[] args) {
     
        for (int i = 0; i < 20; i++) {
     
            threadPoolExecutor.execute(() -> {
     
                try {
     
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            });
        }

        printPoolStatus("before");
        //动态设置
        threadPoolExecutor.setCorePoolSize(4); //可以将动态的值存放在配置中心
        threadPoolExecutor.setMaximumPoolSize(14);
        printPoolStatus("after");

    }

    public static void printPoolStatus(String name) {
     

        System.out.println(name + "核心线程数量:" + threadPoolExecutor.getCorePoolSize() + ""
                + "最大线程数:" + threadPoolExecutor.getMaximumPoolSize());
    }
}

输出结果如下,可以看到动态修改成功。

before核心线程数量:2最大线程数:6
after核心线程数量:4最大线程数:14

动态修改阻塞队列长度

如果我们需要修改队列长度就需要重新实现LinkedBlockingQueue了。因为从代码中我们可以发现队列容量是一个final属性,我们是不能修改的。

所以我们直接复制LinkedBlockingQueue的代码到ReSizeLinkedBlockingQueue中,并对其修改。添加一个设置容量的方法,同时取消final属性。

//设置容量的方法
public void setCapacity(int capacity) {
     
    int oldCapacity = this.capacity;
    this.capacity = capacity;
    int size = count.get();
    if (capacity > size && size >= oldCapacity) {
     
        //唤醒
        signalNotEmpty();
    }
}

/**
     * The capacity bound, or Integer.MAX_VALUE if none
     */
//private final int capacity;
private int capacity;

重新测试:

public class DynamicDemo {
     

    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 6, 60,
            TimeUnit.SECONDS, new ReSizeLinkedBlockingQueue<>(30));

    public static void main(String[] args) {
     
        for (int i = 0; i < 20; i++) {
     
            threadPoolExecutor.execute(() -> {
     
                try {
     
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            });
        }

        printPoolStatus("before");
        //动态设置
        threadPoolExecutor.setCorePoolSize(4); //可以将动态的值存放在配置中心
        threadPoolExecutor.setMaximumPoolSize(14);
        ReSizeLinkedBlockingQueue rb = (ReSizeLinkedBlockingQueue) threadPoolExecutor.getQueue();
        rb.setCapacity(100);
        printPoolStatus("after");

    }

    public static void printPoolStatus(String name) {
     
        ReSizeLinkedBlockingQueue rb = (ReSizeLinkedBlockingQueue) threadPoolExecutor.getQueue();

        System.out.println(name + "核心线程数量:" + threadPoolExecutor.getCorePoolSize() + ""
                + "最大线程数:" + threadPoolExecutor.getMaximumPoolSize() + ""
                + "队列长度:" + (rb.size() + rb.remainingCapacity()));
    }
}

运行结果如下:

before核心线程数量:2最大线程数:6队列长度:30
after核心线程数量:4最大线程数:14队列长度:100

你可能感兴趣的:(并发编程,java,后端,并发编程)