多线程和高并发(七) 线程池

系列文章目录

多线程和高并发(一) Thread
多线程和高并发(二) volatile关键字
多线程和高并发(三) Compare And Swap
多线程和高并发(四) 线程间通信
多线程和高并发(五) ThreadLocal & Lock
多线程和高并发(六) ReentrantLock
多线程和高并发(七) 线程池



多线程和高并发(七) 线程池

捕获线程的执行异常

在线程的run方法中,如果有受检异常必须进行捕获处理,如果想要获取run方法中出现的运行时异常,可以通过回调UncaughtExceptionHandler接口获取哪个线程出现了运行时异常。

setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 设置全局的UncaughtExceptionHandler

getDefaultUncaughtExceptionHandler() 获得全局 的 ( 默认的)UncaughtExceptionHandler

/**
 * @author 奋斗的蜗牛_Z99
 * @CopyRight (C) https://blog.csdn.net/mr_zhu_wenxing?spm=1001.2101.3001.5343
 * @date 2021/5/9
 * @description
 */
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();
    }
/* 
在实际开发中,这种设计异常处理的方式还是比较常用的,尤其是异常执行的方法 

如果线程产生了异常, JVM 会调用 dispatchUncaughtException()方法,在该方法中调用getUncaughtExceptionHandler().uncaughtException(this, e); 如果当前线程设置了 

UncaughtExceptionHandler 回调接口就直接调用它自己的 uncaughtException 方法, 如果没有 

设置则调用当前线程所在线程组UncaughtExceptionHandler 回调接口的uncaughtException 方法,如果线程组也没有设置回调接口,则直接把异常的栈信息定向到 System.err中 
*/
}

注入Hook钩子线程

Hook线程也称为检查线程,当JVM退出的时候会执行Hook线程。经常在程序启动时候生成一个.lock文件,用.lock文件判断是否启动,JVM(程序)退出时候删除.lock文件,Hook线程中除了防止程序冲洗启动,还可以做到资源释放,尽量避免在Hook线程中做复杂操作。

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * @author 奋斗的蜗牛_Z99
 * @CopyRight (C) https://blog.csdn.net/mr_zhu_wenxing?spm=1001.2101.3001.5343
 * @date 2021/5/9
 * @description Hook线程演示
 */
public class Test {
    static Path path = Paths.get("", "tmp.lock");

    public static void main(String[] args) throws InterruptedException {
        //程序退出删除.lock文件
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                path.toFile().delete();
            }
        });

        //程序开启时候判断是否有文件
        if (path.toFile().exists()) {
            //抛出运行时异常
            throw new RuntimeException("程序以启动");
        } else{
            try {
                //第一次启动创建.lock文件
                path.toFile().createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //模拟程序运行 30s 
        Thread.sleep(30000);
    }
}

线程池

什么是线程池

真实生产环境,可能需要很多线程支撑应用,当线程很多时候反而会耗尽CPU资源,消耗来于:创建启动销毁调度线程;

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

JDK对线程池的支持

从代码中看线程池的基本使用

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author 奋斗的蜗牛_Z99
 * @CopyRight (C) https://blog.csdn.net/mr_zhu_wenxing?spm=1001.2101.3001.5343
 * @date 2021/5/9
 * @description 线程池的基本使用
 */
public class Test {

    public static void main(String[] args) throws InterruptedException {
        //创建有 5 个线程大小的线程池
        ExecutorService pool = Executors.newFixedThreadPool(5);

        //向线程池中提交 18 个任务,这 18 个任务存储到线程池的阻塞队列中, 线程池中这 5 个线程就从阻塞队列中取任务执行
        for (int i = 0; i < 18; i++) {
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getId() + " 编号的任务在执 行任务,开始时间: " + System.currentTimeMillis());
                    try {
                        Thread.sleep(3000); //模拟任务执行时长
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @author 奋斗的蜗牛_Z99
 * @CopyRight (C) https://blog.csdn.net/mr_zhu_wenxing?spm=1001.2101.3001.5343
 * @date 2021/5/9
 * @description 线程池的计划任务
 */
public class Test {

    public static void main(String[] args) throws InterruptedException {
        //创建一个有调度功能的线程池 10个线程
        ScheduledExecutorService pool1 = Executors.newScheduledThreadPool(10);
        //在延迟 2 秒后执行任务, schedule( Runnable 任务, 延迟时长, 时间单位)
        pool1.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + " -- " + System.currentTimeMillis());
            }
        }, 2, TimeUnit.SECONDS);
        //以固定的频率执行任务,开启任务的时间是固定的, 在 3 秒后执行任务,以后每隔 2 秒重新执行一次
        pool1.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + "----在固定频率开启任 务---" + System.currentTimeMillis());
                try {
                    TimeUnit.SECONDS.sleep(3);//睡眠模拟任务执行时间 ,如果任务执 行时长超过了时间间隔,则任务完成后立即开启下个任务
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 3, 2, TimeUnit.SECONDS);
    }
}

核心线程池的底层实现

Excutors工具类中返 回 线程池的方法底层都使用了ThreadPoolExecutor线程池,这些方法都是ThreadPoolExecutor线程池的封装.

//ThreadPoolExecutor 的构造方法: 
public ThreadPoolExecutor(int corePoolSize, //指定线程池核心线程数量
int maximumPoolSize, //指定线程池最大线程数量
long keepAliveTime,//超过 corePoolSize且空闲的线程存活时间
TimeUnit unit,//存活时间单位 
BlockingQueue<Runnable> workQueue,//任务队列,把任务提交到该任务队列中等待执行。 
ThreadFactory threadFactory, //线程工厂,用于创造线程
RejectedExecutionHandler handler//拒绝策略,当任务太多来不及处理,如何拒绝
                         ) 
BlockingQueue任务队列

workQueue任务队列是指提交未执行的任务队列,他是BlockingQueue接口的对象,用于存储Runnable任务,根据功能分类,在ThreadPoolExecutor构造中可以有如下几种阻塞队列:

  • 直接任务队列:由SynchronousQueue对象提供,该队列没有容量,提交给线程池的任务不会被真实的保存,总是将新的任务提交给线程执行,如果没有空闲线程则创建新的线程,达到最大线程则执行决绝策略
  • 有界任务队列:由ArrayBlockingQueue实现,在创建ArrayBlockingQueue时候可以指定一个容量,当有任务需要执行时候,任务数量小于corePoolSize则创建新线程,大于corePoolSize时,任务放在ArrayBlockingQueue当中,如果ArrayBlockingQueue已经无法加入,则在maximumPoolSize之中创建新线程,大于maximumPoolSize执行拒绝策略。
  • 无界任务队列:由LinkedBlockingQueue对象实现,与有界队列相比,除非资源耗尽,否则无界队列不存在任务入队失败的情况(在有限资源中队列长度无限大)。当有新的任务时,系统线程数量小于corePoolSize则创建新的线程执行任务;大于corePoolSize把任务加入LinkedBlockingQueue。
  • 优先任务队列:是通过PriorityBlockingQueue实现的,是带有任务优先级的队列,是一个特殊的无界队列。有界任务队列ArrayBlockingQueue,无界任务队列LinkedBlockingQueue都是按照先进先出算法处理任务,优先任务队列PriorityBlockingQueue可以根据优先级顺序先后执行。
RejectedExecutionHandler拒绝策略

当线程池的线程用完了,等待队列也满了,无法为新提交的任务服务,则执行拒绝策略,JDK提供了四种拒绝策略:

  • AbortPolicy策略:直接抛出异常(默认策略)
  • CallerRunsPolicy策略:只要线程池没关闭,会在调用者线程中运行被抛弃的任务。
  • DiscardOldestPolicy策略:将任务队列中最老的任务抛弃,尝试再次提交新任务。
  • DiscardPolicy策略:直接丢弃无法执行的任务。

可以重写rejectedExecution方法,自定义拒绝策略

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), Executors.defaultThreadFactory(), new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { //r 就是请求的任务, executor 就是当前线程池 
        System.out.println(r + " is discarding..");
    }
});
ThreadFactory

ThreadFactory是一个接口,只有一个创建线程的方法Thread newThread(Runnable r);

import java.util.Random;
import java.util.concurrent.*;

/**
 * @author 奋斗的蜗牛_Z99
 * @CopyRight (C) https://blog.csdn.net/mr_zhu_wenxing?spm=1001.2101.3001.5343
 * @date 2021/5/9
 * @description 线程池的计划任务
 */
public class Test {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setDaemon(true);
                System.out.println("创建了线程: " + thread);
                return thread;
            }
        }, new RejectedExecutionHandler() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { //r 就是请求的任务, executor 就是当前线程池
                System.out.println(r + " is discarding..");
            }
        });

        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("奋斗的蜗牛_Z99");
                int num = new Random().nextInt(10);
                System.out.println(Thread.currentThread().getId() + "--" + System.currentTimeMillis() + "开始睡眠:" + num + "秒");
                try {
                    TimeUnit.SECONDS.sleep(num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

       threadPoolExecutor.submit(r).get();
    }
}
监控线程池

ThreadPoolExecutor提供了一组方法用于监控线程池

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

可以给每个任务的开始结束时间自定义一些其他增强的功能,ThreadPoolExecutor 线程池提供了两个方法:

  • protected void afterExecute(Runnable r, Throwable t) :执行某个任务前会调用
  • protected void beforeExecute(Thread t, Runnable r):在任务结束后(任务异常退出)会执行
import java.util.concurrent.*;

/**
 * @author 奋斗的蜗牛_Z99
 * @CopyRight (C) https://blog.csdn.net/mr_zhu_wenxing?spm=1001.2101.3001.5343
 * @date 2021/5/9
 * @description 
 */
public class Test {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        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("线程池退出");
            }
        };
    }
}
优化线程池大小

线程池大小对系统性能是有一定影响的,过大或者过小都会无法发挥最优的系统性能, 线程池大小不需要非常精确,只要避免极大或者极小的情况即可, 一般来说,线程池大小需要考虑 CPU 数量,内存大小等因素。在书中给出一个估算线程池 大小的公式:

线程池大小 = CPU 的数量 * 目标 CPU 的使用率*( 1 + 等待时间与计算时间的比)

线程池死锁

彼此依赖的任务,在同个线程池中可能死锁,可以考虑分别提交给不同的线程池来执行。

线程池中的异常处理

一,submit 提交改为 execute 执行

二,拓展异常处理,对任务进行包装

import java.util.concurrent.*;

/**
 * @author 奋斗的蜗牛_Z99
 * @CopyRight (C) https://blog.csdn.net/mr_zhu_wenxing?spm=1001.2101.3001.5343
 * @date 2021/5/9
 * @description
 */
public class Test {
    private static class TraceThreadPollExecutor extends ThreadPoolExecutor {
        public TraceThreadPollExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        }

        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("客户跟踪异常")));
        }
    }
}
ForkJoinPool 线程池

“分而治之”是一个有效的处理大数据的方法,著名的 MapReduce就是采用这种分而治之的思路. 简单点说,如果要处理的 1000 个数据,但是我们不具备处理1000个数据的能力,可以只处理10个数据, 可以把这 1000 个数据分阶段处理 100 次,每次处理 10 个,把 100 次的处理结果进行合成,形成最后这 1000 个数据的处理结果。把一个大任务调用 fork()方法分解为若干小的任务,把小任务的处理结果进行 join()合并为大任务的结果。

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

ForkJoinPool 线程池中最常用 的方法是: ForkJoinTask submit(ForkJoinTask task) 向线程池提交一个 ForkJoinTask 任务。ForkJoinTask 任务支持 fork()分解与 join()。

ForkJoinTask 有 两 个重 要 的 子 类 :RecursiveAction 和 RecursiveTask,它们的区别在于RecursiveAction任务没有返回值,,RecursiveTask任务可以带有返回值

import java.util.ArrayList;
import java.util.concurrent.*;

/**
 * @author 奋斗的蜗牛_Z99
 * @CopyRight (C) https://blog.csdn.net/mr_zhu_wenxing?spm=1001.2101.3001.5343
 * @date 2021/5/9
 * @description 
 */
public class Test {
    private static class CountTask extends RecursiveTask<Long> {
        private static final int THRESHOLD = 10000;//定义数据规模的阈值,允许计算 10000 个数内的和,超过该阈值的数列就需要分解
        private static final int TASKNUM = 100; //定义每次把大任务分解为 100 个小任务
        private long start; //计算数列的起始值
        private long end; //计算数列的结束值

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

        //重写 RecursiveTask 类的 compute()方法,计算数列的结果
        @Override
        protected Long compute() {
            long sum = 0; //保存计算的结果
            //判断任务是否需要继续分解,如果当前数列 end 与 start 范围的数超过阈值 THRESHOLD,就需要继续分解
            if (end - start < THRESHOLD) {
                //小于阈值可以直接计算
                for (long i = start; i <= end; i++) {
                    sum += i;
                }
            } else {//数列范围超过阈值,需要继续分解
                // 约定每次分解成 100 个小任务,计算每个任务的计算
                long step = (start + end) / TASKNUM;
                //start = 0 , end = 200000, step = 2000, 如果计算[0,200000]范围内数列的 和, 把该范围的数列分解为 100 个小任务,每个任务计算 2000 个数即可
                //注意,如果任务划分的层次很深,即 THRESHOLD 阈值太小,每个任务的计 算量很小,层次划分就会很深,可能出现两种情况:一是系统内的线程数量会越积越多,导致性 能下降严重; 二是分解次数过多,方法调用过多可能会导致栈溢出
                //一个存储任务的集合
                ArrayList<CountTask> subTaskList = new ArrayList<>();
                long pos = start; //每个任务的起始位置
                for (int i = 0; i < TASKNUM; i++) {
                    long lastOne = pos + step; //每个任务的结束位置
                    //调整最后一个任务的结束位置
                    if (lastOne > end) {
                        lastOne = end;
                    }
                    CountTask task = new CountTask(pos, lastOne);
                    //把任务添加到集合中
                    subTaskList.add(task);
                    //调用 for()提交子任务
                    task.fork();
                    //调整下个任务的起始位置
                    pos += step + 1;
                }
                //等待所有的子任务结束后,合并计算结果
                for (CountTask task : subTaskList) {
                    sum += task.join(); //join()会一直等待子任务执行完毕返回 执行结果
                }
            }
            return sum;
        }
    }

    public static void main(String[] args) {
        //创建 ForkJoinPool 线程池
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        //创建一个大的任务
        CountTask task = new CountTask(0L, 200000L);
        //把大任务提交给线程池
        ForkJoinTask<Long> result = forkJoinPool.submit(task);
        try {

            Long res = null;
            try {
                res = result.get();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            //调用任务的 get()方法返回结果
            System.out.println("计算数列结果为:" + res);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //验证
        long s = 0L;
        for (long i = 0; i <= 200000; i++) {
            s += i;
        }
        System.out.println(s);
    }
}

保障线程安全的设计技术

从面向对象设计的角度出发介绍几种保障线程安全的设计技术,这些技术可以使得我们在不必借助锁的情况下保障线程安全,避免锁可能导致的问题及开销.

Java 运行时存储空间

堆空间也非堆空间是线程可以共享的空间,即实例变量与静态变量是线程可以共享的,可能存在线程安全问题。栈空间是线程私有的存储空间,局部变量存储在栈空间中,局部变量具有固定的线程安全性

无状态对象

对象就是数据及对数据操作的封装, 对象所包含的数据称为对象的状态(State), 实例变量与静态变量称为状态变量.

无状态对象不包含任何实例变量也不包含任何静态变量的对象.

不可变对象

  • 类本身使用 final 修饰,防止通过创建子类来改变它的定义
  • 所有的字段都是 final 修饰的,final 字段在创建对象时必须显示初始化,不能被修改
  • 如果字段引用了其他状态可变的对象(集合,数组),则这些字段必须是 private 私有的

线程特有对象

ThreadLocal类相当于线程访问其特有对象的代理,即各个线程通过 ThreadLocal 对象可以创建并访问各自的线程特有对象,泛型 T 指定了线程特有对象的类型. 一个线程可以使用不同的 ThreadLocal 实例来创建并访问不同的线程特有对象

装饰器模式

  • 在 java.util.Collections 工具类中提供了一组 synchronizedXXX(xxx)可以把不是线程安全的 xxx 集合转换为线程安全的集合,它就是采用了这种装饰器模式.
  • 使用装饰器模式的一个好处就是实现关注点分离,在这种设计中, 实现同一组功能的对象的两个版本:非线程安全的对象与线程安全的 对象. 对于非线程安全的在设计时只关注要实现的功能,对于线程安全的版本只关注线程安全性

锁的优化及注意事项

  • 减少锁持有时间
  • 减小锁的粒度
  • 使用读写分离锁代替独占锁
  • 锁分离:如 java.util.concurrent.LinkedBlockingQueue 类中 take()与put()方法分别从队头取数据,把数据添加到队尾
  • 粗锁化
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值