Java 多线程与并发——线程池

使用线程池可以降低资源消耗,提高线程的可管理性。

可以利用 JUC 包(java.util.concurrent)下的 Executors 的静态方法创建不同的线程池:

方法说明
newFixedThreadPool(int nThreads)指定工作线程数量的线程池。
newCachedThreadPool()处理大量短时间工作任务的线程池;
1、试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;
2、如果线程闲置的时机超过阈值,则会被终止并移除缓存;
3、系统长时间闲置的时候,不会消耗什么资源。
newSingleThreadExecutor()创建唯一的工作者线程来执行任务,如果线程异常结束,会有另一个线程取代它。
newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize)定时或者周期性的工作调度,两者的区别在于单一工作线程还是多个线程。
newWorkStealingPool()内部会构建 ForkJoinPool,利用 working-stealing 算法,并行地处理任务,不保证处理顺序。JDK1.7 及以上版本 Java 提供了 Fork/Join 框架,Fork/Join 是把大任务分割成若干个小任务并行执行,最终汇总每个小任务结果后得到大任务结果的框架 。

看下这几个方法的具体实现:

package java.util.concurrent;
public class Executors {
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>()); // 基于链表的阻塞队列
    }
    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>())); // 基于链表的阻塞队列
    }
    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }
    // 省略了部分代码
}

生产环境中不建议使用 Executors 的静态方法创建线程池,建议根据业务自定义 ThreadPoolExecutor ,这里我们封装了一个基于链表的阻塞队列的线程池:

public class LinkedBlockingThreadPool extends ThreadPoolExecutor {
    /**
     * 线程池 基于链表的阻塞队列
     *
     * @param corePoolSize          核心线程数
     * @param maximumPoolSize       最大线程数 maximumPoolSize >= corePoolSize
     * @param keepAliveTime         线程存活时间, 秒
     * @param blockingQueueCapacity LinkedBlockingQueue的容量
     */
    public LinkedBlockingThreadPool(int corePoolSize, int maximumPoolSize, 
                                    long keepAliveTime, int blockingQueueCapacity) {
        super(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                // 当任务队列是LinkedBlockingQueue, 会将超过核心线程的任务放在任务队列中排队
                new LinkedBlockingQueue<Runnable>(blockingQueueCapacity),
                Executors.defaultThreadFactory());
    }
}

LinkedBlockingQueue 一定需要指定大小,因为 LinkedBlockingQueue 的默认容量是 Integer.MAX_VALUE,不指定的话极端情况下会造成大量积压,进而引发 OOM,业务不可用。

ThreadPoolExecutor 构造函数的七个参数:

public class ThreadPoolExecutor extends AbstractExecutorService {
	// ctl的高3位用来保存线程池的运行信息, 另外的低29位保存线程池内有效线程的数量
	private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

    public ThreadPoolExecutor(int corePoolSize,   // 核心线程数
                int maximumPoolSize, // 线程不够用时能够创建的最大线程数 maximumPoolSize>=corePoolSize
                long keepAliveTime, // 核心线程数之外的空闲线程的存活时间, 超时后线程销毁
                TimeUnit unit, // 核心线程数之外的空闲线程的存活时间单位 TimeUnit.SECONDS:秒
                BlockingQueue<Runnable> workQueue, // 任务等待队列
                ThreadFactory threadFactory), // 创建新线程的线程工厂
                RejectedExecutionHandler handler) {  // 线程池饱和策略
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }
    // 省略了部分代码
}

public abstract class AbstractExecutorService implements ExecutorService {
	// 省略代码
}
public interface ExecutorService extends Executor {
	// 省略代码
}
public interface Executor {
    void execute(Runnable command);
}

ThreadFactory 参数表示创建新线程的线程工厂,一般使用默认的 Executors.defaultThreadFactory(),用这个线程工厂创建的新线程具有相同的优先级且是非守护线程。

RejectedExecutionHandler 参数表示线程池饱和策略,如果阻塞队列满了,并且没有空闲线程,这时如果继续提交任务,这时就需要采取一种策略处理该任务,线程池提供了四种策略:

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

除此之外,也可以通过实现 RejectedExecutionHandler 接口的自定义 handler。

ExecutorService 的工作流程:

ExecutorService 的工作流程

新任务提交 execute 执行后的判断流程图:

新任务提交 execute 执行后的判断流程图

线程池的状态:

  • RUNNING:能接受新提交的任务,并且也能处理 workQueue 中的任务;
  • SHUTDOWN:terminated() 方法执行完后进入该状态。不再接受新提交的任务,但可以处理存量任务;
  • STOP:不再接受新提交的任务,也不处理存量任务;
  • TIDYING:所有的任务都已终止;
  • TERMINATED:terminated() 方法执行完后进入该状态。

线程池的状态转换图:

线程池的状态转换图

JUC 的三个 Executor 接口:

  • Executor:运行新任务的简单接口,将任务提交和任务执行细节解耦;
  • ExecutorService:具备管理执行器和任务生命周期的方法,提交任务机制更完善;
  • ScheduledExecutorService:支持 Future 和定期执行任务。

如何设置合理的线程池大小?

首先通过公式预估所需线程池大小:

I/O 密集型应用: 线 程 数 = c p u 核 数 ∗ ( 1 + 线 程 平 均 等 待 时 间 / 线 程 平 均 执 行 时 间 ) 线程数 = cpu 核数*(1 + 线程平均等待时间/线程平均执行时间) 线=cpu(1+线/线)

CPU 密集型应用: 线 程 数 = c p u 核 数 + 1 线程数 = cpu 核数 + 1 线=cpu+1

这两个公式是前人根据大量的经验得出的较为合理的计算方式,然后再通过压测调优,找到最合适的线程池大小。

创建线程池一般不建议使用 Executors 的静态方法去创建,而是应该通过直接构造 ThreadPoolExecutor 的方式,这样的处理方式可以更加明确线程池的运行规则,规避资源耗尽的风险。


线程池配置记录:

机器配置:8 核 16 G,QPS:500,IO 密集型,业务比较重。

Service 层一级并发线程池配置:

核心线程数 corePoolSize:100
最大线程数 maximumPoolSize:200
存活时间 keepAliveTime:5 秒
任务等待队列 workQueue:1500

Service 层二级并发线程池配置:

核心线程数 corePoolSize:200
最大线程数 maximumPoolSize:300
存活时间 keepAliveTime:10 秒
任务等待队列 workQueue:2000

相关推荐
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页