并发编程之线程池

 一、概念

        线程池就是一个复用线程的技术。

        如果不使用线程池,那么用户每发起一个请求,后台就需要创建一个新线程处理,下次新任务来了肯定又要创建新线程处理的,而创建线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。

二、创建线程池的方式

        JDK1.5 起提供了代表线程池的接口:ExecutorService

2.1 获取线程池对象

        方式一:使用 ExecutorService 的实现类 ThreadPoolExecutor 自创建一个线程池对象。

        方式二:使用 Executors (线程池工具类)调用方法返回不同特点的线程池对象。

2.2 ThreadPoolExecutor 构造器

public ThreadPoolExecutor(int corePoolSize,
        int maximumPoolSize,
        long keepAliveTime,
        TimeUnit unit,
        BlockingQueue<Runnable> workQueue,
        ThreadFactory threadFactory,
        RejectedExecutionHandler handler)

        1、corePoolSize:指定线程池的核心线程的数量

        2、maximumPoolSize:指定线程池的最大线程数量

        3、keepAliveTime:指定临时线程的存活时间,假设核心线程数量为 3,最大线程数量为 5,那么临时线程数量就是 2,那么临时线程的存活时间说的就是这 2 个线程的存活时间。

        4、unit:指定临时线程的存活时间单位(秒、分、时、天)

        5、workQueue:指定线程池的阻塞队列

        6、threadFactory:指定线程池的线程工厂,可以为线程起个好名字

        7、handler:指定线程池的任务拒绝策略(线程都在忙且任务队列也满了的时候,新任务来了该如何出来)

2.3 工作原理

        1、线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。

        2、当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入 workQueue 队列排队,直到有空闲的线程。

        3、如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。

        4、如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。

        5、当高峰过去后,超过 corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime unit 来控制。

三、拒绝策略

        拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现。

3.1 jdk 拒绝策略

        1、AbortPolicy:让调用者抛出 RejectedExecutionException 异常,这是默认策略

        2、DiscardPolicy:放弃本次任务

        3、DiscardOldestPolicy:放弃队列中最早的任务,本任务取而代之

        4、CallerRunsPolicy:让调用者运行任务

3.2 其他拒绝策略

        1、Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题。

        2、Netty 的实现,是创建一个新线程来执行任务。

        3、ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略。

        4、PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略

四、线程池分类

4.1 newFixedThreadPool

        固定大小的线程池,构造方法代码如下所示:

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>());
    }

4.1.1 特点

        核心线程数 == 最大线程数(没有临时线程被创建),因此也无需超时时间。并且阻塞队列是无界的,可以放任意数量的任务。

4.1.2 使用场景

        适用于任务量已知,相对耗时的任务。 

4.2 newCachedThreadPool

        缓存的线程池,构造方法代码如下所示:

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>());
    }

4.2.1 特点

        核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着全部都是救急线程(60s 后可以回收)临时线程可以无限创建。

        队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)。

        整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1 分钟后释放线程。

4.2.2 使用场景

         适合任务数比较密集,但每个任务执行时间较短的情况。

4.3 newSingleThreadExecutor

        单例的线程池,构造方法代码如下所示:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>()));
    }

4.3.1 特点

        自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作。

4.3.2 使用场景

        希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

五、提交任务

5.1 执行任务

        语法如下:

// 执行任务
void execute(Runnable command);

        测试代码如下:

@Slf4j(topic = "c.Main")
public class Main{
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        Thread t1 = new Thread(() ->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("我是任务");
        });
        executor.execute(t1);
    }
}

         输出结果如下:

5.2 获取结果

                语法如下:

// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);

        测试代码如下:

@Slf4j(topic = "c.Main")
public class Main{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        Future<String> future = executor.submit(() ->{
            log.debug("running");
            Thread.sleep(1000);
            return "ok";
        });
        log.debug("输出结果为 {}",future.get());
    }
}

         输出结果如下:

5.3 提交所有任务

        语法如下:

// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
    throws InterruptedException;

        测试代码如下:

@Slf4j(topic = "c.Main")
public class Main{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        List<Future<String>> futures = executor.invokeAll(Arrays.asList(
                () -> {
                    log.debug("begin 1");
                    Thread.sleep(1000);
                    return "1";
                },
                () -> {
                    log.debug("begin 2");
                    Thread.sleep(500);
                    return "2";
                },
                () -> {
                    log.debug("begin 3");
                    Thread.sleep(2000);
                    return "3";
                }
        ));

        for (Future<String> future:futures) {
            log.debug("输出结果为:{}",future.get());
        }
    }
}

         输出结果如下:

5.4 提交所有任务带超时时间

        语法如下:

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
    throws InterruptedException;

        测试代码如下:

@Slf4j(topic = "c.Main")
public class Main{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        List<Future<String>> futures = executor.invokeAll(Arrays.asList(
                () -> {
                    log.debug("begin 1");
                    Thread.sleep(1000);
                    return "1";
                },
                () -> {
                    log.debug("begin 2");
                    Thread.sleep(500);
                    return "2";
                },
                () -> {
                    log.debug("begin 3");
                    Thread.sleep(2000);
                    return "3";
                }
        ),1l,TimeUnit.SECONDS);

        for (Future<String> future:futures) {
            log.debug("输出结果为:{}",future.get());
        }
    }
}

        输出结果如下:

5.5 先执行返回

        语法如下:

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
	throws InterruptedException, ExecutionException;

        测试代码如下:

@Slf4j(topic = "c.Main")
public class Main{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        String str = executor.invokeAny(Arrays.asList(
                () -> {
                    log.debug("begin 1");
                    Thread.sleep(1000);
                    return "1";
                },
                () -> {
                    log.debug("begin 2");
                    Thread.sleep(500);
                    return "2";
                },
                () -> {
                    log.debug("begin 3");
                    Thread.sleep(2000);
                    return "3";
                }
        ));
        log.debug("输出结果为:{}",str);
    }
}

        输出结果如下:

5.6 先执行返回带超时时间

        语法如下:

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)
	throws InterruptedException, ExecutionException, TimeoutException;

        测试代码如下:

@Slf4j(topic = "c.Main")
public class Main{
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        String str = executor.invokeAny(Arrays.asList(
                () -> {
                    log.debug("begin 1");
                    Thread.sleep(2000);
                    return "1";
                },
                () -> {
                    log.debug("begin 2");
                    Thread.sleep(3000);
                    return "2";
                },
                () -> {
                    log.debug("begin 3");
                    Thread.sleep(2000);
                    return "3";
                }
        ),1l,TimeUnit.SECONDS);
        log.debug("输出结果为:{}",str);
    }
}

        输出结果如下:

六、关闭线程池

6.1 shutdown

        可以调用 shutdown() 方法来关闭线程池,调用完该方法之后,线程池不会接收新任务,它会将队列中的任务执行完,且不会阻塞调用线程的执行,测试代码如下:

@Slf4j(topic = "c.Main")
public class Main{
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        Future<Integer> result1 = executor.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finsh...");
            return 1;
        });
        Future<Integer> result2 = executor.submit(() -> {
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finsh...");
            return 2;
        });
        Future<Integer> result3 = executor.submit(() -> {
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finsh...");
            return 3;
        });
        log.debug("shutdown");
        executor.shutdown();
        log.debug("不影响其他线程的执行");

    }
}

        输出结果如下:

6.2 shutdownNow

        可以调用 shutdownNow() 方法来关闭线程池,调用完该方法之后,线程池不会接收新任务,它会将队列中的任务返回,并且用 interrupt 的方式中断正在执行的任务。且不会阻塞调用线程的执行,测试代码如下:

@Slf4j(topic = "c.Main")
public class Main{
    public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        Future<Integer> result1 = executor.submit(() -> {
            log.debug("task 1 running...");
            Thread.sleep(1000);
            log.debug("task 1 finsh...");
            return 1;
        });
        Future<Integer> result2 = executor.submit(() -> {
            log.debug("task 2 running...");
            Thread.sleep(1000);
            log.debug("task 2 finsh...");
            return 2;
        });
        Future<Integer> result3 = executor.submit(() -> {
            log.debug("task 3 running...");
            Thread.sleep(1000);
            log.debug("task 3 finsh...");
            return 3;
        });
        log.debug("shutdown");
        List<Runnable> r = executor.shutdownNow();
        log.debug("other....{}",r);
    }
}

        输出结果如下:

七、核心线程数量

        如果线程池中核心线程的数量过小会导致程序不能充分地利用系统资源、容易导致饥饿;过大会导致更多的线程上下文切换,占用更多内存。

7.1 CPU 密集型运算

        cpu 密集型指的就是运算比较多的情况,通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费。

7.2 I/O 密集型运算

        CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程 RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。

        经验公式如下:

线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

        例如 4 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 100% 利用,套用公式:

4 * 100% * 100% / 50% = 8

        例如 4 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 100% 利用,套用公式:

4 * 100% * 100% / 10% = 40

八、任务调度线程池

8.1 Timer 缺点

        在任务调度线程池功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。如下代码:

@Slf4j(topic = "c.Main")
public class Main {
    public static void main(String[] args) {
        Timer timer = new Timer();
        TimerTask task1 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 1");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        };
        TimerTask task2 = new TimerTask() {
            @Override
            public void run() {
                log.debug("task 2");
            }
        };
        // 使用 timer 添加两个任务,希望它们都在 1s 后执行
        // 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的延时,影响了『任务2』的执行
        log.debug("start...");
        timer.schedule(task1, 1000);
        timer.schedule(task2, 1000);
    }
}

        输出结果如下:

8.2 ScheduledExecutorService

        此时我们就可以使用 ScheduledExecutorService 来实现上述的功能,代码如下:

@Slf4j(topic = "c.Main")
public class Main {
    public static void main(String[] args) {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
        // 添加两个任务,希望它们都在 1s 后执行
        executor.schedule(() -> {
            System.out.println("任务1,执行时间:" + new Date());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
        }, 1000, TimeUnit.MILLISECONDS);

        executor.schedule(() -> {
            System.out.println("任务2,执行时间:" + new Date());
        }, 1000, TimeUnit.MILLISECONDS);
    }
}

        可以看到,这两个任务都是在 1s 之后同时执行的。

8.3 scheduleAtFixedRate

        如果想以固定的速率来执行任务,我们就可以使用 scheduleAtFixedRate ,测试代码如下:

@Slf4j(topic = "c.Main")
public class Main {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        log.debug("start...");
        pool.scheduleAtFixedRate(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.debug("running...");
        }, 1, 1, TimeUnit.SECONDS);
    }
}

        输出结果如下,可以看到虽然我们设置了 1s 执行一次,但是任务里面执行了 2s,最终的效果就是 2s 之后才执行一次调度任务,保证了任务的正常执行。

8.4 scheduleWithFixedDelay

        这个线程池也是以固定的速率来执行任务,它和 scheduleAtFixedRate 的区别是,它需要等到任务执行完毕后,再等待固定的时间再去执行任务,如下代码:

@Slf4j(topic = "c.Main")
public class Main {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
        log.debug("start...");
        pool.scheduleWithFixedDelay(() -> {
            log.debug("running...");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }, 1, 1, TimeUnit.SECONDS);
    }
}

        输出结果如下,可以看到,任务执行 2s,延迟 1s,最终任务 3s 之后才开始调度执行。

8.5 定时调度应用

        如何让每周四 18:00:00 定时执行任务?实现代码如下:

@Slf4j(topic = "c.Main")
public class Main {
    public static void main(String[] args) {
        // 获得当前时间
        LocalDateTime now = LocalDateTime.now();
        // 获取本周四 18:00:00.000
        LocalDateTime thursday =
                now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0);
        // 如果当前时间已经超过 本周四 18:00:00.000, 那么找下周四 18:00:00.000
        if (now.compareTo(thursday) >= 0) {
            thursday = thursday.plusWeeks(1);
        }
        // 计算时间差,即延时执行时间
        long initialDelay = Duration.between(now, thursday).toMillis();
        // 计算间隔时间,即 1 周的毫秒值
        long oneWeek = 7 * 24 * 3600 * 1000;
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
        System.out.println("开始时间:" + new Date());
        executor.scheduleAtFixedRate(() -> {
            System.out.println("执行时间:" + new Date());
        }, initialDelay, oneWeek, TimeUnit.MILLISECONDS);
    }
}

九、Fork/Join 线程池

9.1 概念

        Fork/Join JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算。

        所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解。

        Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率。

        Fork/Join 默认会创建与 cpu 核心数大小相同的线程池。

9.2 使用

        提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务,如下代码:

@Slf4j(topic = "c.AddTask")
class AddTask1 extends RecursiveTask<Integer> {
    int n;

    public AddTask1(int n) {
        this.n = n;
    }

    @Override
    public String toString() {
        return "{" + n + '}';
    }

    @Override
    protected Integer compute() {
        // 如果 n 已经为 1,可以求得结果了
        if (n == 1) {
            log.debug("join() {}", n);
            return n;
        }

        // 将任务进行拆分(fork)
        AddTask1 t1 = new AddTask1(n - 1);
        t1.fork();
        log.debug("fork() {} + {}", n, t1);

        // 合并(join)结果
        int result = n + t1.join();
        log.debug("join() {} + {} = {}", n, t1, result);
        return result;
    }

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool(4);
        System.out.println(pool.invoke(new AddTask1(5)));
    }
}

        输出结果如下所示:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐的小三菊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值