本文的开始,我们提出三个最基础的问题:什么是线程,线程与进程的区别,以及如何在JAVA中实现。并做了简要的回答。
一、什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒。Java在语言层面对多线程提供了卓越的支持,它也是一个很好的卖点。
二、线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
三、 如何在Java中实现线程?
Java线程类也是一个object类,它的实例都继承自java.lang.Thread或其子类。 可以用如下方式用java中创建一个线程:
1 | Tread thread = new Thread(); |
执行该线程可以调用该线程的start()方法:
1 | thread.start(); |
在上面的例子中,我们并没有为线程编写运行代码,因此调用该方法后线程就终止了。
编写线程运行时执行的代码有两种方式:一种是创建Thread子类的一个实例并重写run方法,第二种是创建类的时候实现Runnable接口。
回答了以上三个问题之后,那么JAVA中的线程状态是什么呢?请看下面内容
1、新状态:线程对象已经创建,还没有在其上调用start()方法。
2、可运行状态:当线程有资格运行,但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、等待或睡眠状态回来后,也返回到可运行状态。
3、运行状态:线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
4、等待/阻塞/睡眠状态:这是线程有资格运行时它所处的状态。实际上这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到可运行状态。
5、死亡态:当线程的run()方法完成时就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
当然本文的主题是JAVA线程池,那么什么是线程池,线程池相比于单独创建线程来说有什么好处,JAVA如何实现线程池,这些都在下面展现。
之前发布的《阿里巴巴 Java 手册》里有一条
说明了线程池的好处。那么,谈到线程池就会想到池化技术,其中最核心的思想就是把宝贵的资源放到一个池子中;每次使用都从里面获取,用完之后又放回池子供其他人使用,有点吃大锅饭的意思。
那么,JAVA如何实现线程池呢?
在JAVA中,提供了一下四种线程池的实现:
(1)newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
(2)newFixedThreadPool 创建一个固定线程数目的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
(3)newScheduledThreadPool 创建一个有定时功能的线程池,支持定时及周期性任务执行。
(4)newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
追溯这四种线程池的实现,会发现底层实际上还是利用 ThreadPoolExecutor 类实现的。
下面就是我们的重点介绍对象:ThreadPoolExecutor 类
下面是ThreadPoolExecutor最核心的构造方法
Java代码
- 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.corePoolSize = corePoolSize;
- this.maximumPoolSize = maximumPoolSize;
- this.workQueue = workQueue;
- this.keepAliveTime = unit.toNanos(keepAliveTime);
- this.threadFactory = threadFactory;
- this.handler = handler;
- }
构造方法参数讲解
- corePoolSize:线程池核心线程个数
- workQueue:用于保存等待执行的任务的阻塞队列。比如基于数组的有界ArrayBlockingQueue、,基于链表的无界LinkedBlockingQueue,最多只有一个元素的同步队列SynchronousQueue,优先级队列PriorityBlockingQueue,具体可参考 https://www.atatech.org/articles/81568
- maximunPoolSize:线程池最大线程数量。
- ThreadFactory:创建线程的工厂
- RejectedExecutionHandler:饱和策略,当队列满了并且线程个数达到maximunPoolSize后采取的策略,比如AbortPolicy(抛出异常),CallerRunsPolicy(使用调用者所在线程来运行任务),DiscardOldestPolicy(调用poll丢弃一个任务,执行当前任务),DiscardPolicy(默默丢弃,不抛出异常)
- keeyAliveTime:存活时间。如果当前线程池中的线程数量比基本数量要多,并且是闲置状态的话,这些闲置的线程能存活的最大时间
- TimeUnit,存活时间的时间单位
线程管理机制图示:
Executors提供的四种线程池的实现:
1、创建一个固定线程数目的线程池,配置的corePoolSize与maximumPoolSize大小相同,同时使用了一个无界LinkedBlockingQueue存放阻塞任务,因此多余的任务将存在再阻塞队列,不会由RejectedExecutionHandler处理
Java代码
- public static ExecutorService newFixedThreadPool(int nThreads) {
- return new ThreadPoolExecutor(nThreads, nThreads,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
- }
2、创建一个可缓存线程池,配置corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,keepAliveTime=60s,以及一个无容量的阻塞队列 SynchronousQueue,因此任务提交之后,将会创建新的线程执行;线程空闲超过60s将会销毁
Java代码
- public static ExecutorService newCachedThreadPool() {
- return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
- 60L, TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>());
- }
3、创建一个只支持一个线程的线程池,配置corePoolSize=maximumPoolSize=1,无界阻塞队列LinkedBlockingQueue;保证任务由一个线程串行执行
Java代码
- public static ExecutorService newSingleThreadExecutor() {
- return new FinalizableDelegatedExecutorService
- (new ThreadPoolExecutor(1, 1,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>()));
- }
4、创建一个有定时功能的线程池,配置corePoolSize,无界延迟阻塞队列DelayedWorkQueue;有意思的是:maximumPoolSize=Integer.MAX_VALUE,由于DelayedWorkQueue是无界队列,所以这个值是没有意义的
Java代码
- public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
- return new ScheduledThreadPoolExecutor(corePoolSize);
- }
- public static ScheduledExecutorService newScheduledThreadPool(
- int corePoolSize, ThreadFactory threadFactory) {
- return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
- }
- public ScheduledThreadPoolExecutor(int corePoolSize,
- ThreadFactory threadFactory) {
- super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
- new DelayedWorkQueue(), threadFactory);
- }
在ThreadPoolExecutor类中,最核心的任务提交方法是execute()方法,虽然通过submit也可以提交任务,但是实际上submit方法里面最终调用的还是execute()方法,所以我们只需要研究execute()方法的实现原理即可:
public void execute(Runnable command) { if (command == null) throw new NullPointerException(); //获取当前线程池的状态+线程个数变量 int c = ctl.get(); //当前线程池线程个数是否小于corePoolSize,小于则开启新线程运行 if (workerCountOf(c) < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); } //如果线程池处于RUNNING状态,则添加任务到阻塞队列 if (isRunning(c) && workQueue.offer(command)) { //二次检查 int recheck = ctl.get(); //如果当前线程池状态不是RUNNING则从队列删除任务,并执行拒绝策略 if (! isRunning(recheck) && remove(command)) reject(command); //否者如果当前线程池线程空,则添加一个线程 else if (workerCountOf(recheck) == 0) addWorker(null, false); } //如果队列满了,则新增线程,新增失败则执行拒绝策略 else if (!addWorker(command, false)) reject(command); }
- 如果当前线程池线程个数小于corePoolSize则开启新线程
- 否则添加任务到任务队列
- 如果任务队列满了,则尝试新开启线程执行任务,如果线程个数>maximumPoolSize则执行拒绝策略。
小结:
1、用ThreadPoolExecutor自定义线程池,看线程是的用途,如果任务量不大,可以用无界队列,如果任务量非常大,要用有界队列,防止OOM
2、如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务
3、最大线程数一般设为2N+1最好,N是CPU核数
4、核心线程数,看应用,如果是任务,一天跑一次,设置为0,合适,因为跑完就停掉了,如果是常用线程池,看任务量,是保留一个核心还是几个核心线程数
5、如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果的要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM,所以最好异步开个线程获取结果