概述

Java线程的创立,以及线程之间切换上下文时,引入了轻量级锁,倾向锁等技能。意图便是:削减用户态和中心态的切换频率。

可是创立和毁掉线程同样也 十分损耗性能,因为java的线程都会影射到 操作系统底层的实际线程。 为了处理线程重复创立的问题,java中还供给了线程池,以达到线程的复用。

回忆一下,Java的 虚拟机栈,本地办法栈,程序计数器 都是线程私有的。(一切线程共享的是 办法区和堆)当线程创立时,这些东西要创立出来,当线程毁掉时,这些东西又要逐一毁掉。

经过复用线程,能够处理2个问题:

  1. 当异步履行很多使命时,线程池能供给很好的性能。
  2. 线程池供给了资源的限制和管理手法,比方限制线程的个数,动态新增线程等。

线程池的体系

十一、Java线程池刨根问底

中心代码在 ExcutorService 中。

线程池的运用

单线程线程池

为了更方便地创立线程池,JUC供给了一个 Executors 类,它内部供给了多个静态办法让咱们快速创立习惯当时事务场景的线程池。

比方如下场景,单线程池,需求使命逐一履行。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Example {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 6; i++) {
            final int taskId = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() +"->task:" + taskId);
                }
            });
            Thread.sleep(1000);
        }
    }
}

打印成果如下:

pool-1-thread-1->task:0
pool-1-thread-1->task:1
pool-1-thread-1->task:2
pool-1-thread-1->task:3
pool-1-thread-1->task:4
pool-1-thread-1->task:5
pool-1-thread-1->task:6

能够看出,一切的使命都是由 线程池 pool-1 中 的 thread-1 这一个线程去履行的。

带动态缓存的线程池

另一个场景:创立一个可缓存线程池,假如线程池长度超越 当时场景需求(冗余),可灵活回收不需求的线程。


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Example {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 6; i++) {
            final int taskId = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(Thread.currentThread().getName() + "->task:" + taskId);
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                    }
                }
            });
        }
        executorService.shutdown();
    }
}

打印成果为:

pool-1-thread-3->task:2
pool-1-thread-6->task:5
pool-1-thread-5->task:4
pool-1-thread-1->task:0
pool-1-thread-4->task:3
pool-1-thread-2->task:1

线程池 pool-1中,123456号线程全部出动履行使命,使命的打印顺序也不是之前的123456,每次运转顺序都有可能会改变,这便是多线程履行使命时抢夺CPU时刻片导致的不确定成果。

可是假如代码改一下,在提交使命时,先休眠1秒。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Example {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 6; i++) {
            final int taskId = i;
            System.out.println(Thread.currentThread().getName()+" will sleep 1s.");
            Thread.sleep(1000);
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "->task:" + taskId);
                }
            });
        }
        executorService.shutdown();
    }
}

那么打印成果又和单线程线程池一样了。

main will sleep 1s.
main will sleep 1s.
pool-1-thread-1->task:0
main will sleep 1s.
pool-1-thread-1->task:1
main will sleep 1s.
pool-1-thread-1->task:2
main will sleep 1s.
pool-1-thread-1->task:3
main will sleep 1s.
pool-1-thread-1->task:4
pool-1-thread-1->task:5

能够看出,在休眠中的始终是main线程。因为main线程休眠了,导致提交使命呈现1s空档,上一个子线程只需求履行500MS,鄙人一次提交时,本次使命早现已履行完毕,所以打印成果便是依照提交的顺序来了。并且能够看到,这里只呈现了一个线程thread-1,这也是因为,cachedThreadPool是能够动态创立和毁掉线程的,这种场景下,只需求一个线程就足够了。

固定线程数量的可重用线程池

在这个线程中,最多只有3个线程,所以下面的代码中提交了6个使命,最终打印成果也只会呈现3个线程名。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Example {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 6; i++) {
            final int taskId = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "->task:" + taskId);
                }
            });
        }
        executorService.shutdown();
    }
}

打印成果:

pool-1-thread-3->task:2
pool-1-thread-2->task:1
pool-1-thread-1->task:0
pool-1-thread-3->task:4
pool-1-thread-2->task:3
pool-1-thread-1->task:5

最多呈现3个线程,并且线程之间针对CPU时刻片,导致打印成果并不是依照提交使命的顺序。

定时线程池

下面的代码,创立了一个线程数量为2的定时使命线程池。每隔1000MS履行一次使命,首次履行时的延迟时刻为500MS,并且主线程在5S内会封闭线程池,所以最多打印了5次。

package com.example;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Example {
   public static void main(String[] args) throws InterruptedException {
       ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
       executorService.scheduleAtFixedRate(new Runnable() {
           @Override
           public void run() {
               Date date = new Date();
               System.out.println("线程:" + Thread.currentThread().getName() + "报时:" + date);
           }
       }, 500, 1000, TimeUnit.MILLISECONDS);
       Thread.sleep(5000);
       executorService.shutdown();
   }
}

履行成果如下

线程:pool-1-thread-1报时:Wed Oct 11 20:11:48 CST 2023
线程:pool-1-thread-1报时:Wed Oct 11 20:11:49 CST 2023
线程:pool-1-thread-1报时:Wed Oct 11 20:11:50 CST 2023
线程:pool-1-thread-2报时:Wed Oct 11 20:11:51 CST 2023
线程:pool-1-thread-2报时:Wed Oct 11 20:11:52 CST 2023

线程池作业原理

场景类比

举个现实生活中的例子:

这是一个工艺品加工厂,机器三台,订单使命数量目前有4个,那么工厂3台机器都在作业,那么多出来的订单只能在 库房放着。除非有闲暇机器。

十一、Java线程池刨根问底

此刻,工厂为了包容更多订单,可能会考虑新增机器。

十一、Java线程池刨根问底

假如订单持续增多,比方双十一,订单爆仓了,机器满载了,库房也堆不下了,那么此刻,工厂就只能回绝多出来的订单。

线程池的作业原理也类似

  • 咱们能够在创立线程池时指定默许有多少个作业线程
  • 假如使命数量太多,首先会等候现有的机器闲暇出来,此刻多余的订单放在库房中
  • 假如库房都满了,那么多出来的使命就要创立新的线程来履行
  • 可是假如新的线程数量也超出了最大值,那么再多的使命也只能回绝履行了。

上面说到的这些场景,便是 如下图所示的几个结构:

十一、Java线程池刨根问底

中心线程 (默许机器)

works集合,实质是一个hashSet

十一、Java线程池刨根问底

等候行列 (库房)

当中心线程都满负荷之后,也便是说正在作业的中心线程数量超越了 corePoolSize 时,新提交的使命会保存在等候行列中。

十一、Java线程池刨根问底

它的实质是一个堵塞行列,

线程池构造函数参数分析

十一、Java线程池刨根问底

  • corePoolSize 中心线程数
  • maximumPoolSize 线程池可包容的最大线程数
  • keepAliveTime 线程池中线程的等候时刻,假如有非中心线程闲置超越此刻长,则会毁掉。
  • unit 闲暇时刻的单位
  • workQueue 使命等候行列,堵塞行列类型,当请求使命数量大于corePoolSize时,使命会优先放在这个行列中,
  • threadFactofy 线程工厂,假如传入的是null,则会运用默许的DefaultThreadFactory
  • handler 履行回绝战略的目标,当堵塞行列满了,并且 线程数量现已达到了 maximumPoolSize ,就会履行这里的回绝逻辑。

留意:ThreadPoolExecutor 中 allowCoreThreadTimeOut 为true时,假如中心线程超时,它也会被毁掉。只不过默许值是false,会超时毁掉的只有非中心线程。

作业流程

当线程池收到一个使命时:

  • 假如中心线程数没有达到 corePoolSize ,不管其他中心线程是不是闲暇,都会创立出一个中心线程履行使命。
  • 当时线程池中线程数量现已达到了 corePoolSize时,线程池会把使命参加到等候行列中,直到某一个线程闲暇,线程池会依据设置的等候行列规则,从行列中取出一个新的使命履行。

效果如下:

package com.example;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class Example {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        Date date = new Date();
                        System.out.println("线程:" + Thread.currentThread().getName() + "报时:" + date);
                        Thread.sleep(4000);
                    } catch (InterruptedException e) {
                    }
                }
            });
            System.out.println("等候行列中现在有" + executorService.getQueue().size() + "个使命");
            Thread.sleep(500);
        }
    }
}

履行成果:

等候行列中现在有0个使命
线程:pool-1-thread-1报时:Wed Oct 11 20:43:25 CST 2023
等候行列中现在有0个使命
线程:pool-1-thread-2报时:Wed Oct 11 20:43:26 CST 2023
等候行列中现在有1个使命
等候行列中现在有2个使命
等候行列中现在有3个使命
线程:pool-1-thread-1报时:Wed Oct 11 20:43:29 CST 2023
线程:pool-1-thread-2报时:Wed Oct 11 20:43:30 CST 2023
线程:pool-1-thread-1报时:Wed Oct 11 20:43:33 CST 2023
  • 线程数现已达到了corePoolSize数量可是还没有达到maximunPoolSize并且等候行列已满的时分,会创立非中心线程来履行使命
package com.example;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Example {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(2,
                10,
                0, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(2));
        for (int i = 0; i < 5; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Date date = new Date();
                        System.out.println("线程:" + Thread.currentThread().getName() + "报时:" + date);
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                    }
                }
            });
            System.out.println("等候行列中现在有" + executorService.getQueue().size() + "个使命");
            Thread.sleep(500);
        }
    }
}

履行成果:

等候行列中现在有0个使命
线程:pool-1-thread-1报时:Wed Oct 11 20:49:29 CST 2023
等候行列中现在有0个使命
线程:pool-1-thread-2报时:Wed Oct 11 20:49:30 CST 2023
等候行列中现在有1个使命
等候行列中现在有2个使命
等候行列中现在有2个使命
线程:pool-1-thread-3报时:Wed Oct 11 20:49:31 CST 2023
线程:pool-1-thread-1报时:Wed Oct 11 20:49:31 CST 2023
线程:pool-1-thread-2报时:Wed Oct 11 20:49:32 CST 2023
  • 最后假如提交到使命,无法被中心线程履行,也不能参加等候行列,也不能创立新的非中心线程来履行,线程池将会依据回绝处理战略来处理它。

将最大线程数改为3,中心线程数仍然为2,下面的代码中,履行一次使命需求5000MS,而6个使命是一次性提交进去的,其间第四个使命就会因为 无法被中心线程履行,无法参加等候行列,无法创立新的非中心线程履行,而履行回绝战略。

package com.example;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Example {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(2,
                3,
                0, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(2));
        for (int i = 0; i < 6; i++) {
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Date date = new Date();
                        System.out.println("线程:" + Thread.currentThread().getName() + "报时:" + date);
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                    }
                }
            });
            System.out.println("等候行列中现在有" + executorService.getQueue().size() + "个使命");
            Thread.sleep(500);
        }
    }
}
线程:pool-1-thread-1报时:Wed Oct 11 20:52:13 CST 2023
等候行列中现在有0个使命
线程:pool-1-thread-2报时:Wed Oct 11 20:52:13 CST 2023
等候行列中现在有1个使命
等候行列中现在有2个使命
等候行列中现在有2个使命
线程:pool-1-thread-3报时:Wed Oct 11 20:52:15 CST 2023
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.example.Example$1@f6f4d33 rejected from java.util.concurrent.ThreadPoolExecutor@23fc625e[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]
        at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2070)
        at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:833)
        at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1365)
        at com.example.Example.main(Example.java:17)
线程:pool-1-thread-1报时:Wed Oct 11 20:52:18 CST 2023
线程:pool-1-thread-2报时:Wed Oct 11 20:52:18 CST 2023

实际上这种直接抛出异常的战略,只是java供给了4种战略中的一种:

十一、Java线程池刨根问底

并且能够自定义回绝战略。

为了阿里明令禁止运用 Executors 东西类 创立线程

虽然线程池如此优雅地为咱们管理了 使命的履行,可是假如无节制地运用 Executors 会导致崩溃,内存溢出等问题。

比方说: Executors.newFixedThreadPool(2)创立了一个固定数量为2的线程池,当使命添加超越一定数量时,可能会产生OOM内存溢出,这是因为 newFixedThreadPool 默许会创立无限容量的 堵塞行列来暂存使命,堵塞行列的size是由一个int值表示的,它的最大值是 2^16-1,理论上,假如size超越了这个数,就再也无法刺进使命到行列中。

再比方:Exceutors.newCachedThreadPool() 的问题类似,因为它也是运用默许的不指定size的线程池,也就意味着能够无限创立线程,当有无限多使命去提交的时分,它会不加节制地创立线程。而一个操作系统中,对每个进程可运用的线程数量都是有限制的,一旦超越这个数,就无法再继续创立。

所以要正确运用线程池,仍是老老实实用

new ThreadPoolExecutor(2,
                3,
                0, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(2));

这种写法指定线程池的各项参数,尤其是 线程数量,等候行列的容量。这样才能确保利用线程池处理事务问题的一起,不给自己埋雷。