Java多线程—线程池
一、引入
采用非线程池方式来开启新线程,例如:继承Thread类,实现Runnable接口,实现Callable接口,这三种方式都是有一个任务,就创建一个线程来执行,任务执行完之后,就销毁该线程,下一次还有任务,就再创建一个线程来执行;时常创建、销毁线程带来的资源浪费十分不合理。
举一个简单的例子:比如我们在使用一次性筷子,一双筷子用完就扔了,下一次再要用筷子的时候就再买一双一次性筷子,这就好比原来的非线程池方式来创建线程,“一次性”这个词很适合形容这种方式。如果我们不使用一次性筷子,我们家里的多次使用的筷子,都放在筷笼里,每次吃饭时就从里面拿出来用,吃完饭,把筷子洗一洗就放进筷笼里,长期使用。
线程池就类似这个筷笼,程序员创建一个线程池(ThreadPool),每当提交任务的时候,线程池就会创建一个线程来执行任务,当任务执行完之后,该线程又会回到线程池里,等待下一次分配。
二、线程池主要核心原理
1-线程池工作结构图
核心流程:
- 程序员创建一个线程池,这个线程池是空的
- 提交一个任务,线程池就创建一个线程来执行该任务,任务执行完之后,线程放回线程池中,等待下一次复用
- 如果线程池里的线程都在使用中,有新的任务提交,那么就要在任务队列里等待空闲线程
三、快速上手
我们使用Excutors工具类来调用线程池创建方法,然后创建线程。我们一般常用的线程池有以下两种:
- 无上限线程池:newCacheThreadPool
- 有上限线程池:newFixedThreadPool
1、newCacheThreadPool
public class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("线程"+Thread.currentThread().getName()
+"正在执行"+"--"+i);}}
}
public class Test {public static void main(String[] args) throws InterruptedException {//创建一个没有上限的线程池(最大线程数量是int类型的上限)ExecutorService executorService = Executors.newCachedThreadPool();//提交任务executorService.submit(new MyRunnable());executorService.submit(new MyRunnable());executorService.submit(new MyRunnable());}
}
创建一个无上限的线程池,并提交了三个任务,每个任务循环打印一百次内容,我们可以看到下面的输出:
因为我们同时提交了三个任务,可以看出来有三个线程在执行任务
我们接下来稍微改一下代码,看看线程池里的线程能不能复用,我们让main线程在每次任务提交之后睡眠1秒,确保每次任务都能在下一次任务提交前执行完
public class MyRunnable implements Runnable{@Overridepublic void run() {System.out.println("线程"+Thread.currentThread().getName()+"正在执行");}
}
public class Test {public static void main(String[] args) throws InterruptedException {//创建一个没有上限的线程池(最大线程数量是int类型的上限)ExecutorService executorService = Executors.newCachedThreadPool();//提交任务executorService.submit(new MyRunnable());Thread.sleep(1000);executorService.submit(new MyRunnable());Thread.sleep(1000);executorService.submit(new MyRunnable());}
}
我们可以看到,线程池里一直是同一个线程在执行任务,达到了线程复用的目的
2、newFixedThreadPool
public class Test {public static void main(String[] args) throws InterruptedException {//创建一个自定义上限的线程池,设置最大线程数量为3ExecutorService pool = Executors.newFixedThreadPool(3);//提交任务pool.submit(new MyRunnable());pool.submit(new MyRunnable());pool.submit(new MyRunnable());pool.submit(new MyRunnable());}
}
创建一个自定义线程数量上限的线程池,并设置最大线程数量不超过3,然后我们提交了四个任务,输出结果中只有三个线程在工作
四、自定义线程池
我们在上面的快速上手中采用Executors工具类来创建的线程池,我们也能创建自定义的线程池,创建ThreadPoolExecutor类并通过构造方法的参数来初始化这个线程池,下面来介绍下这个方法的各种参数。
为了方便理解各个参数的意义,这里我们举个例子来介绍:假设有个餐厅,餐厅里有三个正式员工和三个临时员工,正式员工永远都不会被开除,临时员工如果餐厅一直没有顾客,那就会被开除。这个餐厅每个员工只能服务一位顾客,只有当前服务的顾客离开之后,才能服务另一位顾客,当这六名员工都在服务顾客的时候,此时如果再有新来的顾客,那么就应该在店门口排队,当排队人数到达一定的数量时,这个餐厅为了能确保每个人都能吃上饭,所以它就规定队伍人数达到上限的时候就不再接受新来的顾客了。
参数一:核心线程数量 int corePoolSize
- 核心线程意思是在这个线程池中永远不会被销毁的线程,好比上面例子里的正式员工,初创线程池的时候是没有线程的,当有任务陆续来临的时候,线程池开始创建核心线程,但是核心线程数目具有上限。
参数二:线程池中最大线程数量 int maximumPoolSize
- 最大线程数量 = 核心线程数量 + 临时线程数量
参数三:空闲时间(值) long keepAliveTime
参数四:空闲时间(单位) TimeUnit unit
- 空闲时间指的是临时线程在多长时间内没有被使用,就会被销毁
参数五:阻塞队列 BlockingQueue<Runnable> workQueue
- 阻塞队列意思是当核心线程都在执行任务时,新提交的任务就要进入阻塞队列,我们可以在创建阻塞队列的时候定义队列的长度
参数六:线程工厂 ThreadFactory threadFactory
- 线程工厂用来表示线程池通过什么方式创建线程
参数七:拒绝策略 RejectedExecutionHandler handler
- 当线程池里的线程都在执行任务,并且阻塞队列中的任务数量已达上限,那么就会执行拒绝策略,来处理新提交的任务
任务拒绝策略有以下几种方式:
1.ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出RejectedExecutionException异常(默认策略)
2.ThreadPoolExecutor.DiscardPolicy 丢弃任务但是不抛出异常(不推荐)
3.ThreadPoolExecutor.DiscardOldestPolicy 抛弃队伍中等待时间最久的任务,把当前任务加入
4.ThreadPoolExector.CallerRunsPolicy 调用任务的run方法,绕过线程池执行
五、自定义线程池工作流程
延续上面的例子,假如我定义了核心线程有3个,临时线程有3个,阻塞队列长度为3,这个时候来了10个任务,线程池处理的方式如下:
任务一、二、三都会被核心线程执行,然后任务四、五、六进入阻塞队列,任务七、八、九会被临时线程执行,任务十触发任务拒绝策略
2-线程池工作流程图
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, //核心线程数量 6, //最大线程数量 60, //空闲时间(值) TimeUnit.SECONDS, //空闲时间(单位)new ArrayBlockingQueue<>(3), //等待队列,队列长度为3Executors.defaultThreadFactory(), //线程工厂new ThreadPoolExecutor.AbortPolicy() //拒绝策略 );