个人笔记 Java多线程(八)

这段笔记是参照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 线程组基本操作

  1. int activeCount()返回当前线程组及子线程中活动线程的数量(近似值).
  2. int activeGroupCount()放回当前线程组及子线程组中活动线程组的数量(近似值).
  3. int enumerate(Thread[] list)将当前线程组和子线程组中的所有线程复制到参数数组中。int enumerate(Thread[] list,boolean recursive)如果第二个参数设置为false,则只复制当前线程组中所有的线程,不复制子线程组中的线程。
  4. int enumerate(ThreadGroup[] list)将当前线程组中所有线程组复制到参数数组中。int enumerate(ThreadGroup group,boolean recursive)第二个参数设置为false,则不复制子线程组。
  5. int getMaxPriority()返回线程组的最大优先级,默认为10。
  6. String getName()返回线程组的名称。
  7. String getParent()返回父线程组。
  8. void interrupt()中断线程组中所有的线程。
  9. boolean isDaemon()判断当前线程组是否为守护线程组。
  10. void list()将当前线程组中的活动线程打印到控制台。
  11. boolean parentOf(ThreadGroup group)判断当前线程组是否为参数线程组的父线程。
  12. 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中有关处理运行异常的方法有:

  1. getDefaultUncaughtExceptionHandler()实例方法可以获得全局的(默认的)UncaughtException异常。
  2. static getUncaughtExceptionHandler()静态方法获得当前线程的UncaughtException异常。
  3. setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)设置全局的UncaughtExceptionHandler。
  4. 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资源。如果对线程不进行控制与管理,然而会影响程序的性能。

线程开销主要包括:

  1. 创建与启动线程的开销
  2. 线程销毁的开销
  3. 线程调度的开销
  4. 线程数量受限于CPU内核的数量

线程池就是有效使用线程的一种常用方式,线程池在内部可以预先创建一定数量的工作线程,客户端代码可以直接将任务作为对象提交给线程池,线程池将任务缓存在工作队列中,线程池中的任务线程就不断的从队列中取出任务并执行。

3.1 线程池的基本使用

  • 基本实现
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) 
  1. corePoolSize,指定线程池中核心线程的数量。
  2. maximumPoolSize,指定线程池中最大线程数量。
  3. keepAliveTime,当线程池数量超过corePoolSize数量时,多余的空闲线程的存活时长,空闲线程在多长时间内销毁。
  4. unit,是keepAliveTime的时长单位。
  5. workQueue,任务队列,把任务提交到任务队列等待执行。
  6. threadFactory,线程工厂,用于创建线程。
  7. handler,拒绝策略,当任务太多来不及处理的时候如何拒绝。

说明:workQueue工作队列是指提交未执行的任务队列,它是BlockingQueue接口对象,仅用于存储Runnable任务。根据队列功能分类,在ThreadPoolExecutor构造方法中可以使用以下几种阻塞队列:

  1. 直接提交队列,由SynchronousQueue对象提供,该队列没有容量,提交给线程池的任务不会被真实的保存,总是将新的任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程。如果数量已经打到maximumPoolSize规定的最大值,就执行拒绝策略。
  2. 有界提交队列,由ArrayBlockingQueue实现,在创建ArrayBlockingQueue对象的时候可以指定一个容量,当有任务需要执行的时候,如果线程池中线程数小于corePoolSize核心线程数则创建新的线程,如果大于核心线程数则加入等待队列。如果队列已满则无法加入,在线程数小于maximumPoolSize指定的最大线程数的情况下会创建新的线程来执行,如果大于maximumPoolSize规定的最大线程数的前提下执行拒绝策略。
  1. 无界提交队列,由LinkedBlockingQueue对象实现,与有界队列相比,除非系统资源耗尽,负责无界队列不存在任务入队失败的情况。当有新的任务时,在系统线程数小于corePoolSize核心线程数则创建新的线程来执行任务;当线程池中线程数量大于corePoolSize核心线程数则把任务加入阻塞队列。
  2. 优先任务队列,通过PriorityBlockingQueue实现的,是带有任务优先级的队列,是一个特殊的无界队列。不管是ArrayBlockingQueue还是LinkedBlockingQueue都是按照先进先出算法来处理任务的,在PriorityBlockingQueue队列中是按照任务优先级顺序执行的。

3.3 拒绝策略

ThreadPoolExecutor构造方法的最后一个参数指定了拒绝策略,当提交给线程池的任务量超过实际承载能力时,即当线程池中线程已经用完了,等待策略也满了,无法为新提供的任务提供服务,可以通过拒绝策略来处理这个问题,JDK提供了四种拒绝策略:

  1. AbortPolicy策略,会抛出异常。默认的拒绝策略
  2. CallerRunsPolicy策略,只要线程池没有关闭,就会在调用者线程中运行当前被丢弃的任务。
  3. DiscardOldersPolicy策略,会将任务队列中最老的任务(马上要执行的任务)丢弃,并尝试提交新任务。
  4. 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提供了一组方法用于监控线程池:

  1. int getActiveCount()获得线程池中当前活动线程的数量。
  2. long getCompleteTaskCount()返回线程池完成任务的数量。
  3. int getCorePoolSize()返回线程池核心线程的数量
  4. int getLargestPoolSize()返回线程池曾经达到的最大线程数
  5. int getMaximumPoolSize()返回线程池的最大容量
  6. int getPoolSize()返回当前线程池大小
  7. BlockingQueue<Runnable> getQueue()返回阻塞队列
  8. long getTaskCount()返回线程池收到的任务总数

3.8 拓展线程池

有时需要对线程池进行拓展,如监控每个任务的开始结束时间、自定义增强一些功能。

ThreadPoolThread线程池提供了两个方法:

  1. protected void afterExecute(Runnable r,Throwable t)
  2. 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线程池中的异常处理

线程池中发生运行时异常可能会被程序吞掉,即不会报错不会显示报错信息。解决方法是:

  1. 要么把submit()改成execute()
  2. 要么对线程池进行拓展,对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()就是大任务的处理结果。

系统对ForkJoinPool线程池进行了优化,提交的任务数量与线程的数量不一定是一点一的关系。在多数情况下,一个物理线程需要处理多个逻辑任务。

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();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值