简单入门线程池(三)

简单入门线程池(一)中,对于线程池的定义,和Java对线程池的支持,以及线程池的构造方法和涉及的参数这些方面,对线程池进行了简单的介绍。
简单入门线程池(二)中,对线程池的内部实现,执行流程以及任务队列,做了进一步的介绍。
接着,将对线程池进一步做一些拓展。

1. 扩展线程池

ThreadPoolExecutor是一个可拓展的线程池。
它提供了beforeExecute( )、afterExecute( )和terminated( )三个接口对线程池进行控制。
以beforeExecute( )、afterExecute( )为例,在ThreadPoolExecutor.Worker.runTask( )方法内部提供了这样的实现。

boolean ran = false;
// 运行前
beforeExecute(thread, task);
// 运行任务
try {
	task.run();
	ran = true;
	// 运行结束后
	afterExecute(task, null);
	++completedTasks;
} catch (RuntimeException e) {
	if (!ran) {
	// 运行结束
		afterExecute(task, e);
		throw e;
	}
}

ThreadPoolExecutor.Worker是ThreadPoolExecutor的内部类,是一个实现了Runnable接口的类。ThreadPoolExecutor线程池中的工作线程也正是Worker实例。Worker.runTask( )方法会被线程池以多线程模式异步调用,即Worker.runTask( )会同时被多个线程访问。

实例

/**
 * @author LISHANSHAN
 * @ClassName ExtThreadPool
 * @Description TODO
 * @date 2022/06/2022/6/1 21:33
 */

public class ExtThreadPool {

    public static class MyTask implements Runnable{
        public String name;

        public MyTask(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println("正在执行" + ": Thread ID: " + Thread.currentThread().getId()
             + ", Task Name = " + name);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {

        ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>()) {
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("准备执行:" + ((MyTask)r).name);
            }
            @Override
            protected void afterExecute(Runnable r, Throwable tr) {
                System.out.println("执行完成:" + ((MyTask)r).name);
            }
            @Override
            protected void terminated() {
                System.out.println("线程池退出");
            }
        };
        for (int i = 0; i < 5; i++) {
            MyTask task = new MyTask("TASK-GEYM-" + i);
            es.execute(task);
            Thread.sleep(100);
        }
        es.shutdown();
    }
}

执行结果

在这里插入图片描述
可以发现,所有任务的执行前、执行后的时间点以及任务的名字都可以被捕获,这对于应用程序的调试有利。

优化线程池的数量

线程池的大小对系统的性能有一定的影响。
过大或过小的线程数量都无法发挥最优的系统性能。
一般来说,确定线程池的大小需要考虑CPU数量、内存大小等因素。
在Java中,可以通过

Runtime.getRuntime().availableProcessors();

获得CPU的数量。
在《Java Concurrency in Pactice》中,给出了估算线程池大小的经验公式:
N C P U = C P U 的数量 U C P U = C P U 的使用率 ( 0 ≤ U C P U ≤ 1 ) W / C = 等待时间与计算时间的比率 则可得最佳的线程池的大小 : N t h r e a d s = N C P U × U C P U × ( 1 + W C ) N_{CPU} = CPU的数量\\ U_{CPU} = CPU的使用率(0 \leq U_{CPU} \leq 1)\\ W/C = 等待时间与计算时间的比率\\ 则可得最佳的线程池的大小:\\ Nthreads = N_{CPU} \times U_{CPU} \times (1 + \frac{W}{C}) NCPU=CPU的数量UCPU=CPU的使用率(0UCPU1)W/C=等待时间与计算时间的比率则可得最佳的线程池的大小:Nthreads=NCPU×UCPU×(1+CW)
一个简单而且适用面比较广的公式:

  • CPU密集型任务(N+1):这种任务消耗的主要是CPU资源,可以将线程数设置为N(CPU核心数) + 1, 比CPU核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其他原因导致的任务暂停而带来的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下就可以充分利用CPU的空闲时间
  • I/O密集型任务(2N):这种任务应用起来,系统会用大量时间来处理I/O交互,而线程在处理I/O的时间段内不会占用CPU,这时就可以将CPU交出给其他线程使用。因此,可以多配置一些线程。

2. 寻找异常堆栈

多线程本身就很容易引发一些错误,更何况如今使用线程池呢。
也就是说,线程池很有可能会“吃掉”程序抛出的一场,导致我们对程序的错误一无所知。
那么,如何向线程池讨回异常堆栈呢?

用execute( )

即在提交任务时,放弃submit,而是改用execute( )方法。
pools.execute(new MyTask());
或者Future ft = pools.submit(new MyTask());
ft.get( );
这两种方式,都可以得到部分堆栈的内容,即可以知道异常是在哪里抛出的,但是却无法知道任务是在何处提交的。因为任务的具体提交位置已经被线程池淹没了,只可以知道任务的调度流程。

案例代码

import com.sun.deploy.trace.Trace;

import java.util.concurrent.*;

/**
 * @author LISHANSHAN
 * @ClassName TraceThreadPoolExecutor
 * @Description TODO
 * @date 2022/06/2022/6/1 22:43
 */

public class TraceThreadPoolExecutor extends ThreadPoolExecutor {

    public static class DivTask implements Runnable{
        int a, b;
        public DivTask(int a, int b) {
            this.a = a;
            this.b = b;
        }
        @Override
        public void run() {
            double re = a/b;
            System.out.println(re);
        }
    }

    public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                                   long keepAliveTime, TimeUnit unit,
                                   BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    public void execute(Runnable command) {
        super.execute(wrap(command, clientTrace(), Thread.currentThread().getName()));
    }

    @Override
    public Future<?> submit(Runnable command) {
        return super.submit(wrap(command, clientTrace(), Thread.currentThread().getName()));
    }

    private Exception clientTrace() {
        return new Exception("Client stack trace");
    }
//该wrap函数中的第2个参数为异常,里边保存着提交任务的线程的堆栈信息,该方法将我们传入的Runnable任务进行一层包装,使之能处理异常信息。
    private Runnable wrap(final Runnable command, final Exception clientTrace,
                          String clientThreadName) {
        return new Runnable() {
            @Override
            public void run() {
                try {
                    command.run();
                } catch (Exception e) {
                    clientTrace.printStackTrace();
                    throw e;
                }
            }
        };
    }

    public static void main(String[] args) {
        ThreadPoolExecutor pools = new TraceThreadPoolExecutor(0, Integer.MAX_VALUE,
                0L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
        for (int i = 0; i < 5; i++) {
            pools.execute(new DivTask(100, i));
        }
    }
}

运行结果

在这里插入图片描述

3. 分而治之:Fork/Join框架

如果要处理1000个数据,那么可以在先只处理其中的10个,然后分阶段处理100次,再将100次的结果进行合成,就是最终想要的对原始1000个数据的处理结果。

简析

fork和Join

  1. 函数fork( )用来创建子进程,使得系统进程可以多一个执行分支。
  2. join( )也就是等待,使用fork( )后系统多了一个执行分支(线程),所以需要等待这个执行分支执行完毕,才有可能得到最终的结果。
  3. 在JDK中,给出了一个ForkJoinPool线程池。对于fork方法并不急着开启线程,而是提交给ForkJoinPool线程池进行处理,以节省系统资源。

“互帮互助”

由于线程池的优化,提交的任务和线程的数量并不是对等的关系。在绝大多数情况下,一个线程实际上是要处理多个逻辑任务的。因此,每个线程必然要有一个任务队列。因此在实际执行时,可能会出现,A的任务执行完了,但是B还有一堆任务要执行,那么A就会来帮助B,从B的任务队列中取出任务来处理。值得注意的是,当线程试图帮助别人时,总是从任务队列的底部开始拿数据,而线程试图执行自己的任务时,则是从相反顶部开始。这样可以避免线程之间发生数据冲突。

重要接口

ForkJoinPool的一个重要接口:

public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task)

所谓ForkJoinTask任务就是支持fork分解和join等待的任务。
ForkJoinTask有两个重要的子类,RecursiveAction和RecursiveTask。它们分别表示没有返回值的任务和可以携带返回值的任务。
在这里插入图片描述

简例

public class CountTask extends RecursiveTask<Long> {
	// 最大任务量
    private static final int THRESHOLD = 10000;
    private long start;
    private long end;

    public CountTask(long start, long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        long sum = 0;
        boolean canCompute = (end - start) < THRESHOLD;
        // 如果在计算能力内,则直接计算
        if (canCompute) {
            for (long i = start; i <= end; i++) {
                sum = sum + i;
            }
        } else {
            // 反之,分成100个小任务,每个任务的范围,即为步长
            long step = (end - start) / 100;
            ArrayList<CountTask> subTasks = new ArrayList<>();
            long pos = start;
            for (int i = 0; i < 100; i++) {
                long lastOne = pos + step;
                if (lastOne > end) {
                    lastOne = end;
                }
                // 计算当前任务
                CountTask subTask = new CountTask(pos, lastOne);
                // 更新任务的起始位置
                pos = pos + step + 1;
                // 将当前任务加入列表中
                subTasks.add(subTask);
                // 将当前任务进行分解。
                subTask.fork();
            }
            // 等待列表中的所有任务执行完毕
            for (CountTask t : subTasks) {
                sum = sum + t.join();
            }
        }
        // 将结果返回
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        CountTask task = new CountTask(0, 2000L);
        // 提交任务
        ForkJoinTask<Long> result = forkJoinPool.submit(task);

        try {
        // 将结果转化为long类型
            long res = result.get();
            System.out.println("sum = " + res);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException ec) {
            ec.printStackTrace();
        }
    }
}

注意

划分层次

在使用ForkJoin时要注意,如果任务的划分层次很深,一直得不到返回,那么可能出现两种情况:
第一、系统内的线程数量越积越多,严重影响性能
第二、函数的调用层次变得很深,最终导致栈溢出

无锁的栈

此外,ForkJoin线程池使用一个无锁的栈来管理空闲线程,如果下一个工作线程暂时取不到可用的任务,那么可能会被挂起,挂起的线程会压入由线程池维护的栈中。等到有任务可用时,再从栈中被唤醒

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值