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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/smartbetter/article/details/51866792

为什么要使用线程池?

  • 降低资源消耗;
  • 提高线程的可管理性。

可以利用 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);
    }
    // 省略了部分代码...
}

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() 方法执行完后进入该状态。

线程池的状态转换图:

线程池的状态转换图

ScheduledThreadPoolExecutor 的参数:

package java.util.concurrent;
public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor
        implements ScheduledExecutorService {
    public ScheduledThreadPoolExecutor(int corePoolSize) {
    	// 也是调用了ThreadPoolExecutor的构造方法
        super(corePoolSize,  //核心线程数
        	Integer.MAX_VALUE, //线程不够用时能够创建的最大线程数
        	0, //核心线程数之外的空闲线程的存活时间
        	NANOSECONDS, //核心线程数之外的空闲线程的存活时间单位
            new DelayedWorkQueue()); //任务等待队列
    }
}

public interface ScheduledExecutorService extends ExecutorService {
	// 省略代码
}

JUC 的三个 Executor 接口:

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

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

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

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

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

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

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

展开阅读全文

没有更多推荐了,返回首页