线程池概念
池: 容器,
以数据库连接池为例:
每次连接都需要创建一个对象,用完销毁,频繁地创建销毁占用一定的开销。因此有了池的概念,就是先创建一定数量的对象放入数据库连接池有连接过来时,从池子中获取一个连接对象使用,用完之后不销毁,换回到池子中减少创建销毁的开销。
例:使用jdbc链接5000次使用时间20s,和使用阿里德鲁伊数据源连接5000次的0.6s比速度慢了很多。
为什么要使用线程池:池(容器)
每次创建线程都会占用内存空间,如果无线的创建内存就会浪废掉内存空间,验证的情况就可能导致内存溢出
cpu是有限的,一个cpu只能处理一个线程,很多线程没有执行权,那这些线程都会出去等待,会造成线程间大量的切换,导致性能变慢。因此需要线程池,创建管理线程。
线程池里的每一个线程在使用结束后不会销毁,就处于空闲状态,等待下一个任务的到来。
怎么使用线程池?java提供了有哪些线程池的实现?
-
jdk5版本java内置了线程池实现ThreadPolExecutor,
-
还提供Executors来创建不同类型的线程池。
Executors类(不推荐使用)
在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见的有四种
1.创建使用固定线程数(大小)的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
-
核心线程数与最大线程数一样,没有救急线程
-
阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
-
适用场景:适用于任务量已知,相对耗时的任务
2.单线程化的线程池,一个单线程的线程池,只有池子里一个线程,保证所有任务按顺序执行。
ExecutorService exec =Executors.newSingleThreadExecutor();
-
核心线程数和最大线程数都是1
-
阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
-
适用场景:适用于按照顺序执行的任务
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class NewSingleThreadCase { static int count = 0; static class Demo implements Runnable { @Override public void run() { count++; System.out.println(Thread.currentThread().getName() + ":" + count); } } public static void main(String[] args) throws InterruptedException { //单个线程池,核心线程数和最大线程数都是1 ExecutorService exec =Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { exec.execute(new Demo()); Thread.sleep(5); } exec.shutdown(); } }
-
可缓存线程池:会根据任务自动新增或回收线程
-
核心线程数为0
-
最大线程数是Integer.MAX_VALUE
-
阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都
-
必须等待一个移出操作。
-
适用场景:适合任务数比较密集,但每个任务执行时间较短的情况
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class CachedThreadPoolCase { static class Demo implements Runnable { @Override public void run() { String name = Thread.currentThread().getName(); try { //修改睡眠时间,模拟线程执行需要花费的时间 Thread.sleep(100); System.out.println(name + "执行完了"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { //创建一个缓存的线程,没有核心线程数,最大线程数为 Integer.MAX_VALUE ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { exec.execute(new Demo()); Thread.sleep(1); } exec.shutdown(); } }
为什么不建议用Executors创建线程池
参考阿里开发手册《Java开发手册-嵩山版》
ThreadPoolExecutor
提供了“延迟”和“周期执行”功能的ThreadPoolExecutor。
使用ThreadPoolExecutor的好处?
ThreadPoolExecutor准确的控制创建线程池的数量,最大等待数量,拒绝策略等。
适用场景:有定时和延迟执行的任务
import java.util.Date; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ScheduledThreadPoolCase { static class Task implements Runnable { @Override public void run() { try { String name = Thread.currentThread().getName(); System.out.println(name + ", 开始:" + newDate()); Thread.sleep(1000); System.out.println(name + ", 结束:" + new Date()); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { //按照周期执行的线程池,核心线程数为2,最大线程数为 Integer.MAX_VALUE ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2); System.out.println("程序开始:" + new Date()); /** * schedule 提交任务到线程池中 * 第一个参数:提交的任务 * 第二个参数:任务执行的延迟时间 * 第三个参数:时间单位 */ scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS); scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS); scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS); Thread.sleep(5000); // 关闭线程池 scheduledThreadPool.shutdown(); } }
ThreadPoolExecutor的(核心参数)
参考ThreadPoolExecutor这个类的7个参数的构造方法
-
corePoolSize 核心线程数目(核心池子大小),创建线程池后,默认线程是0(先不创建线程),有任务来创建线程 ,之后就不销毁了存在线程池。也可以调用perstartAllCoreThreads()或者perstartCoreThreads()方法进行预创建线程。
-
maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)
-
keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
-
unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
-
workQueue(等待队列) - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
-
threadFactory 线程工厂 - 线程对象的创建,例如设置线程名字、是否是守护线程等
-
handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,就触发拒绝策略
执行原理
1.任务在提交的时候,先判断核心线程数是否已满,未满则添加到工作线程执行任务。
2.如果核心线程数满了,则判断阻塞队列是否已满,未满,则进入阻塞队列。
3.如果阻塞队列也满了,判断当前线程总数是否小于最大线程数,小于,则创建救急线程,也就是临时线程去执行任务,如果核心和临时线程执行完了,回去阻塞队列中找是否有要被执行的任务,有则去执行。
4.如果所有线程都在忙(核心线程+临时线程),则使用拒绝策略处理。
拒绝策略
1.AbortPolicy:直接抛出异常,默认策略;
2.CallerRunsPolicy:用调用者所在的线程来执行任务(main);
3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4.DiscardPolicy:直接丢弃任务;
execute 与 submit 的区别
执行任务除了可以使用 execute 方法还可以使用 submit 方法。它们的主要区别
是:execute 适用于不需要关注返回值的场景,submit 方法适用于需要关注返
回值的场景。
线程池中常见的阻塞队列
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会
创建救急线程执行任务
常用的有:
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
ArrayBlockingQueue的LinkedBlockingQueue区别
LinkedBlockingQueue | ArrayBlockingQueue |
---|---|
默认无界,支持有界 | 强制有界 |
底层是列表 | 底层是数组 |
是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
入队会生成新 Node | Node需要是提前创建好的 |
两把锁(头尾) | 一把锁 |
左边是LinkedBlockingQueue加锁的方式,右边是ArrayBlockingQueue加锁的方 式
LinkedBlockingQueue读和写各有一把锁,性能相对较好
ArrayBlockingQueue只有一把锁,读和写公用,性能相对于 LinkedBlockingQueue差一些
确定核心线程数
执行线程池执行任务的类型:
IO密集型任务:文件读写,DB读写,网络请求
推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)
CPU密集型任务 :计算型代码、Bitmap转换、Gson转换等
推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)
java代码查看cpu核数:
public static void main(String[] args) { System.out.println(Runtime.getRuntime().availableProcessors()); }
① 高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换
② 并发不高、任务执行时间长
IO密集型的任务 --> (CPU核数 * 2 + 1)
计算密集型任务 --> ( CPU核数+1 )
③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整
体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器
是第二步。
CountDownLatch
CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)
-
其中构造参数用来初始化等待计数值
-
await() 用来等待计数归零
-
countDown() 用来让计数减一
关闭线程池
关闭线程池可以调用 shutdownNow 和 shutdown 两个方法来实现。
shutdownNow:对正在执行的任务全部发出 interrupt(),停止执行,对还未开
始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:当我们调用 shutdown 后,线程池将不再接受新的任务,但也不会
去强制终止已经提交或者正在执行中的任务。
ThreadLocal类
ThreadLocal(线程变量)是多线程中用于解决线程安全的一个操作类,他会为每个线程分配一个单独只属于当前线程的线程副本,从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内的资源共享。
例:
使用jdbc操作数据库时,会将每一个线程的Connection放入各自的threadlocal中,从而保证每个线程都在各自的connection上进行数据库操作,避免a线程关闭b线程。
基本使用:
set(value)设置值,
get()获取设置的值
remove()清除值
ThreadLocal的实现原理
ThreadLocal本质就是一个线程内部的存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离。
在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象value,放入当前线程的 ThreadLocalMap 集合中
当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
ThreadLocal内存泄漏问题:
是应为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC回收,释放key,此时ThreadLocalMap中key=null,而value还强引用着,不会被回收,只有当前线程结束才会被回收。
解决:
在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,需要我们每次使用完 ThreadLocal 都调用它的 remove()方法清除数据。