这段笔记是参照b站教程BV1Rv411y7MU整理而来的,用于个人备忘以便复习,需要的朋友可以自取。
线程管理
1.线程组
类似于计算机中使用文件夹管理文件,可以使用线程组来管理线程。在线程组中定义一组相关(类似)的线程,在线程组中可以定义子线程组。
Thread类有几个构造方法允许在创建线程的时候指定线程组,如果在创建线程时没有指定线程租,则该线程则属于父线程所在的线程组。JVM在创建main线程的时候会为它指定一个线程组,因此每个Java线程都有一个与之相关联,可以调用getThreadGroup()
方法返回线程组。
线程早期处于安全考虑,设计用来区分不同的Applet,然而ThreadGroup并未实现这一目标,在新开发的系统中不常用线程组!现在一般会将一组相关的线程存入一个数组或者集合之中,如果仅仅用来区分线程时,可以使用线程名称来区分。多数情况下可以忽略线程组。
ThreadGroup(String name);//自动归属到当前线程的线程组中
ThreadGroup(ThreadGroup parent,String name);
public static void main(String[]args){
//返回当前main线程
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
System.out.println(mainGroup);
//定义线程组,如果不指定所属线程组,则自动归属到当前线程所属线程组
ThreadGroup gourp1 = new ThreadGroup("group1");
System.out.println(gourp1);
//创建一个线程组,同时指定父线程组
ThreadGroup group2 = new ThreadGroup(mainGroup,"group2");
//现在group1和group2都时mainGroup的子线程组
System.out.println(gourp1.getParent() == mainGroup);
System.out.println(group2.getParent() == mainGroup);
//创建线程时,指定所属的线程组
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
//在创建线程的时候没有指定线程组,则默认将线程归属到父线程的线程组中
Thread thread = new Thread(r,"t1"); //由于没有指定线程组,所以默认归属到当前线程main的线程组中
System.out.println(thread);//Thread[t1,5,main]
Thread thread1 = new Thread(group2,r,"t2");
Thread thread2 = new Thread(group2,r,"t3");
System.out.println(thread1);
System.out.println(thread2);
}
1.1 线程组基本操作
int activeCount()
返回当前线程组及子线程中活动线程的数量(近似值).int activeGroupCount()
放回当前线程组及子线程组中活动线程组的数量(近似值).int enumerate(Thread[] list)
将当前线程组和子线程组中的所有线程复制到参数数组中。int enumerate(Thread[] list,boolean recursive)
如果第二个参数设置为false,则只复制当前线程组中所有的线程,不复制子线程组中的线程。int enumerate(ThreadGroup[] list)
将当前线程组中所有线程组复制到参数数组中。int enumerate(ThreadGroup group,boolean recursive)
第二个参数设置为false,则不复制子线程组。int getMaxPriority()
返回线程组的最大优先级,默认为10。String getName()
返回线程组的名称。String getParent()
返回父线程组。void interrupt()
中断线程组中所有的线程。boolean isDaemon()
判断当前线程组是否为守护线程组。void list()
将当前线程组中的活动线程打印到控制台。boolean parentOf(ThreadGroup group)
判断当前线程组是否为参数线程组的父线程。void setDaemon()
设置当前线程组为守护线程组
1.2 复制线程组
public class Test01 {
public static void main(String[] args) {
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
ThreadGroup group1 = new ThreadGroup(mainGroup,"g1");
ThreadGroup group2 = new ThreadGroup("g2");
Runnable r = new Runnable() {
@Override
public void run() {
while (true){
System.out.println("---当前线程: "+Thread.currentThread().getName());
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t1 = new Thread(r,"t1");//默认在main线程组中创建线程
Thread t2 = new Thread(group1,r,"t2");
Thread t3 = new Thread(group2,r,"t3");
t1.start();
t2.start();
t3.start();
//把main线程组中线程复制到数组中
//数组长度为main线程组中活动线程数量
Thread[] threads = new Thread[mainGroup.activeCount()];
//把main线程组中包括子线程组中的线程复制到参数中
mainGroup.enumerate(threads);
for (Thread thread:threads){
System.out.println(thread);
}
System.out.println("===================");
mainGroup.enumerate(threads,false);
for (Thread thread:threads){
System.out.println(thread);
}
System.out.println("===================");
//把main线程组中子线程组复制到数组中
//定义数组存储线程组
ThreadGroup[] groups = new ThreadGroup[mainGroup.activeGroupCount()];
//把main线程中子线程组复制到数组中
mainGroup.enumerate(groups);
for (ThreadGroup threadGroup:groups){
System.out.println(threadGroup);
}
}
}
1.3 线程组批量中断
线程组的interrupt()可以给线程组中所有的活动线程添加中断标志。
public class Test02 {
public static void main(String[] args) throws InterruptedException {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("当前线程: -- "+Thread.currentThread().getName()+" -- 开始循环");
while(!Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+"----------------");
/*
//如果中断睡眠中的线程,
//会产生中断异常,同时还会清楚中断标志
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
*/
}
System.out.println(Thread.currentThread().getName()+"循环结束");
}
};
ThreadGroup group = new ThreadGroup("group");
//在group线程组中创建5个线程
for (int i = 0; i < 5; i++) {
new Thread(group,r,"Thread_"+i).start();
}
Thread.sleep(50);
group.interrupt();
}
}
1.4 设置守护线程组
守护线程是为其他线程服务的,当JVM中只有守护线程时,守护线程会自动销毁,JVM退出。
调用线程组的setDaemon()
可以把线程组设置为守护线程组。当守护线程组中没有任何活动线程的时候,守护线程组会自动销毁。
注意线程组中的守护属性,不影响线程组中线程的守护属性,或者说守护线程组中的线程可以是非守护线程。
OpenJDK16中setDaemon()已经被废弃!
@SuppressWarnings("all")
@Deprecated
public class Test03 {
public static void main(String[] args) throws InterruptedException {
ThreadGroup group = new ThreadGroup("group");
//设置线程组为守护线程组
group.setDaemon(true);//OpenJDK16版本被废弃
for (int i = 0; i < 3; i++) {
new Thread(group, new Runnable() {
@Override
public void run() {
for (int j=0;j<20;j++){
System.out.println(Thread.currentThread().getName()+" -- "+ j);
try{
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"Thread_"+i).start();
}
Thread.sleep(5000);
System.out.println("main...end...");
}
}
2.捕获线程的执行异常
在线程的run()方法中,如果有受检异常必须进行补货处理,如果想要获得run()方法中出现运行时异常信息,可以通过回调UncaughtExceptionHandler接口获得哪个线程出现了运行异常。在Thread中有关处理运行异常的方法有:
getDefaultUncaughtExceptionHandler()
实例方法可以获得全局的(默认的)UncaughtException异常。static getUncaughtExceptionHandler()
静态方法获得当前线程的UncaughtException异常。setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
设置全局的UncaughtExceptionHandler。static setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
设置当前线程的UncaughtExceptionHandler。
当线程运行出现异常的时候,JVM会调用Thread类的dispatchUncaughtException(Throwable)
方法,该方法会调用getUncaughtExceptionHandler().uncaughtException(this,e);
如果想要获得线程中出现异常的信息,就需要设置线程的UncaughtExceptionHandler。
2.1 设置线程异常的回调接口
public class Test {
public static void main(String[] args) {
//设置全局的回调接口
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
//t参数接受发生异常的线程,e就是线程中的异常
System.out.println(t.getName()+" 线程产生了异常: "+e.getMessage());
}
});
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" -- 开始运行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
//线程中的受检异常必须捕获处理
e.printStackTrace();
}
System.out.println(12/0);
}
});
t1.start();
new Thread(new Runnable() {
@Override
public void run() {
String txt = null;
System.out.println(txt.length());
}
}).start();
}
}
在实际开发中,这种设计异常处理的方式还是比较常用的,尤其是异步执行的方法。
2.2 注入Hook钩子线程
很多软件如MySQL,Zookeeper,kafka都存在Hook线程的校验机制,目的是检测进程是否已经启动,防止重复程序。、
Hook线程也叫做钩子线程,当JVM退出的时候会执行Hook线程。经常在程序启动的时候创建一个.lock文件,用这个.lock文件来校验程序是否启动。在程序退出(JVM退出)的时候再删除该.lock文件。
在Hook线程中除了防止重新启动线程外,还可以做资源释放,但是尽量避免在Hook线程中进行复杂的操作。
public class Hook {
public static void main(String[] args) throws IOException, InterruptedException {
//注入Hook线程,在程序退出的时候删除.lock文件
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
System.out.println("JVM退出会启动当前Hook线程,并在Hook线程中删除.lock文件");
getLockFile().toFile().delete();
}
});
//程序运行的时候检查lock文件是否存在
//如果lock文件存在则抛出异常
if(getLockFile().toFile().exists()){
throw new RuntimeException("程序已经启动");
}else {
//文件不存在,说明程序第一次启动,则创建.lock文件
getLockFile().toFile().createNewFile();
System.out.println("程序在启动时创建了.lock文件");
}
//模拟程序运行
for (int i = 0; i < 100; i++) {
System.out.println("程序正在运行");
TimeUnit.SECONDS.sleep(1);
}
}
private static Path getLockFile(){
return Paths.get("","tmp.lock");
}
}
3.线程池
可以以new Thread(()->{线程执行任务}).start();
这种形式开启一个线程。当run()方法运行结束后,线程对象会被GC释放。
在真实的生产环境中,可能需要很多的线程来支撑整个应用,当线程数量非常多的时候,反而会耗尽CPU资源。如果对线程不进行控制与管理,然而会影响程序的性能。
线程开销主要包括:
- 创建与启动线程的开销
- 线程销毁的开销
- 线程调度的开销
- 线程数量受限于CPU内核的数量
线程池就是有效使用线程的一种常用方式,线程池在内部可以预先创建一定数量的工作线程,客户端代码可以直接将任务作为对象提交给线程池,线程池将任务缓存在工作队列中,线程池中的任务线程就不断的从队列中取出任务并执行。
![](https://i-blog.csdnimg.cn/blog_migrate/78d66887b1e9c159026ac410f4e5d3a6.png)
3.1 线程池的基本使用
![](https://i-blog.csdnimg.cn/blog_migrate/1e03ae71e117db2a191fb8d465f2055d.png)
- 基本实现
public class Test01 {
public static void main(String[] args) {
//创建有5个线程大小的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
//向线程池提交18个任务
//这18个线程就储存到线程池的阻塞队列中,线程池中这5个线程就从阻塞队列中取任务执行
for (int i = 0; i < 18; i++) {
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId()+" -- 执行任务 "+System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
}
- 线程池计划任务
public class Test02 {
public static void main(String[]args){
//创建一个有调度功能的线程池
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
//延迟2s后执行任务
//schedule(Runnable任务,延迟时长,时间单位)
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId()+" -- "+System.currentTimeMillis());
}
},2, TimeUnit.SECONDS);
//以固定的频率执行任务,开始任务的时间是固定的
//3s后开启任务,之后每隔5s执行一次
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getId()+" -- "+System.currentTimeMillis());
//模拟任务,如果任务执行时间超过了时间间隔,则任务完成后立刻开启下个任务
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},3,2,TimeUnit.SECONDS);
}
}
3.2 线程池底层实现
查看Executors工具类中newCachedThreadPool(),newSingleThreadPool(),newFixedThreadPool()源码后Executors工具类返回线程池的方法底层都返回了ThreadPoolExecutor线程池。,这些方法都是ThreadPoolExecutor线程池的封装。
ThreadPoolExecutor构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize,指定线程池中核心线程的数量。
- maximumPoolSize,指定线程池中最大线程数量。
- keepAliveTime,当线程池数量超过corePoolSize数量时,多余的空闲线程的存活时长,空闲线程在多长时间内销毁。
- unit,是keepAliveTime的时长单位。
- workQueue,任务队列,把任务提交到任务队列等待执行。
- threadFactory,线程工厂,用于创建线程。
- handler,拒绝策略,当任务太多来不及处理的时候如何拒绝。
说明:workQueue工作队列是指提交未执行的任务队列,它是BlockingQueue接口对象,仅用于存储Runnable任务。根据队列功能分类,在ThreadPoolExecutor构造方法中可以使用以下几种阻塞队列:
- 直接提交队列,由SynchronousQueue对象提供,该队列没有容量,提交给线程池的任务不会被真实的保存,总是将新的任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程。如果数量已经打到maximumPoolSize规定的最大值,就执行拒绝策略。
- 有界提交队列,由ArrayBlockingQueue实现,在创建ArrayBlockingQueue对象的时候可以指定一个容量,当有任务需要执行的时候,如果线程池中线程数小于corePoolSize核心线程数则创建新的线程,如果大于核心线程数则加入等待队列。如果队列已满则无法加入,在线程数小于maximumPoolSize指定的最大线程数的情况下会创建新的线程来执行,如果大于maximumPoolSize规定的最大线程数的前提下执行拒绝策略。
![](https://i-blog.csdnimg.cn/blog_migrate/81303932dfc9b0e3d123ccfe1b919788.png)
- 无界提交队列,由LinkedBlockingQueue对象实现,与有界队列相比,除非系统资源耗尽,负责无界队列不存在任务入队失败的情况。当有新的任务时,在系统线程数小于corePoolSize核心线程数则创建新的线程来执行任务;当线程池中线程数量大于corePoolSize核心线程数则把任务加入阻塞队列。
- 优先任务队列,通过PriorityBlockingQueue实现的,是带有任务优先级的队列,是一个特殊的无界队列。不管是ArrayBlockingQueue还是LinkedBlockingQueue都是按照先进先出算法来处理任务的,在PriorityBlockingQueue队列中是按照任务优先级顺序执行的。
3.3 拒绝策略
ThreadPoolExecutor构造方法的最后一个参数指定了拒绝策略,当提交给线程池的任务量超过实际承载能力时,即当线程池中线程已经用完了,等待策略也满了,无法为新提供的任务提供服务,可以通过拒绝策略来处理这个问题,JDK提供了四种拒绝策略:
- AbortPolicy策略,会抛出异常。默认的拒绝策略
- CallerRunsPolicy策略,只要线程池没有关闭,就会在调用者线程中运行当前被丢弃的任务。
- DiscardOldersPolicy策略,会将任务队列中最老的任务(马上要执行的任务)丢弃,并尝试提交新任务。
- DiscardPolicy策略,丢弃无法提交的任务。
Executors工具类提供的静态方法返回的线程池默认的拒绝策略时AbortPolicy策略!
如果提供的拒绝策略无法满足需求,可以自定义拓展需求接口。
public class Test03 {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
int num=new Random().nextInt(10);
System.out.println(Thread.currentThread().getId()+" -- "+System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10), Executors.defaultThreadFactory(), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//r为请求方法,executor为当前线程池
System.out.println(r+"is discarding...");
}
});
for (int i = 0; i < Integer.MAX_VALUE; i++) {
threadPoolExecutor.submit(r);
}
}
}
3.6 ThreadFactory线程工厂
ThreadFactory是一个接口,只有一个用来创建线程的方法:Thread newThread(Runnable r);
当线程池需要创建线程的时候,就需要调用该方法。
- 自定义线程工厂:
public class Test04 {
public static void main(String[] args) throws InterruptedException {
Runnable r = new Runnable() {
@Override
public void run() {
int num = new Random().nextInt(10);
System.out.println(Thread.currentThread().getId()+" --begin sleep "+System.currentTimeMillis());
try {
TimeUnit.SECONDS.sleep(num);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getId()+" --end--");
}
};
//创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {//参数r即提交的任务
//创建一个线程
Thread t = new Thread(r);
t.setDaemon(true);//设置为守护线程
return t;
}
});
//提交五个任务
for (int i = 0; i < 5; i++) {
threadPoolExecutor.submit(r);
}
Thread.sleep(5000);
//主线程睡眠,主线程结束,线程池中线程自动退出(因为设置为了守护线程)
}
}
3.7 监控线程池
ThreadPoolExecutor提供了一组方法用于监控线程池:
int getActiveCount()
获得线程池中当前活动线程的数量。long getCompleteTaskCount()
返回线程池完成任务的数量。int getCorePoolSize()
返回线程池核心线程的数量int getLargestPoolSize()
返回线程池曾经达到的最大线程数int getMaximumPoolSize()
返回线程池的最大容量int getPoolSize()
返回当前线程池大小BlockingQueue<Runnable> getQueue()
返回阻塞队列long getTaskCount()
返回线程池收到的任务总数
3.8 拓展线程池
有时需要对线程池进行拓展,如监控每个任务的开始结束时间、自定义增强一些功能。
ThreadPoolThread线程池提供了两个方法:
protected void afterExecute(Runnable r,Throwable t)
protected void beforeExecute(Thread t,Runnable r)
在线程池执行某个任务前会调用beforeExecute结束后(包括任务异常退出)会调用afterExecute方法
ThreadPoolExecutor源码中定义了一个内部类Worker,ThreadPoolExecutor线程池中的工作线程就是Worker类的实例。Worker实例在执行时,也会调用beforeExecute和afterExecute方法。
public class Test05 {
public static class MyTask implements Runnable{
private String name;
public MyTask(String name){this.name = name;}
@Override
public void run() {
System.out.println(this.name+" 任务正在被线程 "+Thread.currentThread().getId()+" 执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
//定义一个拓展线程池
ExecutorService executorService = new ThreadPoolExecutor(5,5,0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()){
//内部类中重写开始结束方法
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println(t.getId()+" --线程开始执行任务");
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println(" --线程结束任务");
}
@Override
protected void terminated() {
System.out.println("线程池退出");
}
};
for (int i = 0; i < 5; i++) {
MyTask myTask = new MyTask("Task"+i);
executorService.submit(myTask);
}
//关闭线程池(线程不会继续接受新的任务,但是线程池已有的任务正常执行完毕)
executorService.shutdown();
}
}
3.9 优化线程池的数量
线程池的大小对系统性能是有一定影响的,过大或者过小都会导致无法发挥最优的系统性能,线程大小不需要非常精确,只要避免极大或者极小的情况即可。一般来说,线程池大小考虑CPU数量,内存大小等因素,在《java Concurrency in Practice》书中给出一个估算线程池大小的公式:
线程池大小 = CPU数量目标CPU的使用率(1+等待时间与计算时间的比)
3.10 线程池死锁
如果在线程池中执行了任务A的执行过程中又向线程池提交了任务B,任务B添加到线程池的等待队列中,如果认为任务A的结束需要等待任务B的执行结果,就有可能出现:线程池中所有的工作线程都处于等待任务处理结果,而这些任务在阻塞队列中等待执行。
适合给线程池提交相互独立的任务,而不是彼此依赖的任务。对于相互依赖的任务可以提交个不同的线程池来执行。
3.11线程池中的异常处理
线程池中发生运行时异常可能会被程序吞掉,即不会报错不会显示报错信息。解决方法是:
- 要么把submit()改成execute()
- 要么对线程池进行拓展,对submit()进行封装
- 修改submit()
public class Test06 {
public static class DivideTask implements Runnable{
private int x,y;
public DivideTask(int x,int y){
this.x = x;
this.y = y;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getId()+" --计算x/y= "+(this.x/this.y));
}
}
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0,Integer.MAX_VALUE,0, TimeUnit.SECONDS,new SynchronousQueue<>());
for (int i = 0; i < 5; i++) {
threadPoolExecutor.execute(new DivideTask(10,i));
}
/**
* 运行程序会产生算数异常,我们实际向线程池提供了5体数据,但是发生异常的数据没有打印
* 解决方法为: 把提交方法submit()改为execute()
*/
}
}
- 重新封装submit()
public class Test07 {
//自定义线程池类,并对任务进行包装
private static class TraceThreadPoolExecutor extends ThreadPoolExecutor{
public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
//定义方法,对执行的方法进行包装
//接受两个参数,第一个参数接受要执行的任务,第二个参数是对Exception异常
public Runnable wrap(Runnable task,Exception exception){
return new Runnable() {
@Override
public void run() {
try{
task.run();
}catch (Exception e){
exception.printStackTrace();
throw e;
}
}
};
}
//重写submit
@Override
public Future<?> submit(Runnable task){
return super.submit(wrap(task,new Exception("客户跟踪异常")));
}
//重写execute
@Override
public void execute(Runnable command) {
super.execute(wrap(command, new Exception("客户跟踪异常")));
}
}
//任务接口
public static class DivideTask implements Runnable{
private int x,y;
public DivideTask(int x,int y){
this.x = x;
this.y = y;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getId()+" --计算x/y= "+(this.x/this.y));
}
}
public static void main(String[] args) {
//使用自定义线程池
ThreadPoolExecutor threadPoolExecutor = new TraceThreadPoolExecutor(0,Integer.MAX_VALUE,0,TimeUnit.SECONDS,new SynchronousQueue<>());
for (int i = 0; i < 5; i++) {
threadPoolExecutor.submit(new DivideTask(10,i));
}
}
}
3.12 ForkJoinPool线程池
"分而治之"是一个有效的处理大数据的方法,著名的MapReduce就是采用这种分而治之的思想。
简单来说,如果要处理1000个数据,但是我们不具备处理1000个数据的能力,只能处理10个数据。对此就可以每次只处理10个数据,分批处理100次,最后把100次的结果进行合成,形成1000个数据的处理结果。
把一个大任务调用fork()方法分解成若干小任务,把小任务的处理结果join()就是大任务的处理结果。
![](https://i-blog.csdnimg.cn/blog_migrate/b8264cae766ddc451ea1f3c1190f26be.png)
系统对ForkJoinPool线程池进行了优化,提交的任务数量与线程的数量不一定是一点一的关系。在多数情况下,一个物理线程需要处理多个逻辑任务。
![](https://i-blog.csdnimg.cn/blog_migrate/2bcb1ebc1716f533edb423a6365ac7da.png)
ForkJoinPool线程池中最常用的方法是sumbit(ForkJoinTask<T> task)
支持提交fork分解和join等待的任务。ForkJoinTask有两个重要的子类:RecursiveAction和RecursiveTask。其区别在于Action没有返回值而Task可以带有返回值。
public class Test08 {
//计算数列的和,需要返回结果,可以定义任务继承RecursiveTask
private static class Task extends RecursiveTask<Long>{
private static final int THRESHOLD = 10000;//定义数据规模阈值,运行计算1w个数据和,超过就分解
private long begin;
private long end;
private final static int TASK_NUM = 100;//定义每次大任务分解为100个小任务
public Task(long begin,long end){
this.begin = begin;
this.end = end;
}
@Override
protected Long compute() {
long sum = 0;//保存计算结果
//判断任务是否需要继续分解,如果当前数列begin和end中的数超过阈值就分解
if((this.end-this.begin)<THRESHOLD){
for(long i = this.begin;i<=this.end;i++){
sum += i;
}
}else{
//继续分解
//约定每次分解100个小任务,计算每个任务的数值
long step = (this.end-this.begin)/100;
//注意:如果任务划分很深,即阈值设置太小,那么每个任务计算量很小,层次划分就会很深
//这种情况下可能出现两种情况:1.系统内线程数量越来越多导致性能下降2.分解过多导致栈溢出
ArrayList<Task> subTaskList = new ArrayList<>();
long pos = this.begin;
for (int i = 0; i < TASK_NUM; i++) {
long lastOne = pos+step;//每个任务的结束位置
//调整最后一个
if (lastOne > this.end){
lastOne = this.end;
}
//创建子任务
Task task = new Task(pos,lastOne);
//添加任务到集合中
subTaskList.add(task);
//调用fork()提交子任务
task.fork();
//调整下个任务的起始位置
pos += step+1;
}
//等待所有的子线程任务结束后,合并计算结果
for (Task task:subTaskList){
sum+=task.join();//join会一直等待子任务执行完毕并返回执行结果
}
}
return sum;
}
}
public static void main(String[] args) {
//创建ForkJoinPool线程池
ForkJoinPool forkJoinPool = new ForkJoinPool();
//创建一个大任务
Task task = new Task(0L,200000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(task);
try{
long res = submit.get();//调用get()方法返回结果
System.out.println("数列返回结果为: "+res);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
}