小白架构师成长之路15-Executor线程池详解

Executor线程池详解

大家里面的案例可以从gitHub下载下来自己看一下
地址:https://github.com/JolyouLu/Juc-study.git 代码在Juc-Executor下

什么是进程和多线程

进程,进程本质上也是一个线程,我们在电脑每打开一个应用就开启一个进程,java中的main方法主函数就相当于一个进程,在运行我们的主函数时我们可以开启额外的线程来异步处理我们的一些业务,这就是进程于线程的关系
在这里插入图片描述

实现多线程的方式

Thread

//继承Thread 重写run实现多线程
public class MyThread extends Thread{

    @Override
    public void run() {
        System.out.println("继承Thread 重写run实现多线程");
    }

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

Runnable

//实现Runnable接口实现run方法
public class MyRunnable implements Runnable{

    public void run() {
        System.out.println("实现Runnable接口实现run方法");
    }

    public static void main(String[] args) {
        //Runnable没有start方法他需要借助Thread启动
        new Thread(new MyRunnable()).start();
    }
}

Callable

//实现Callable 重写call方法 可带参数返回
public class MyCallable implements Callable<String> {

    public String call() throws Exception {
        return "实现Callable 重写call方法 可带参数返回";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask task = new FutureTask(new MyCallable());
        new Thread(task).start();
        System.out.println(task.get());
    }
}

总结

实现Runnable接口相比继承Thread类有如下又是

  1. 可以避免由于 Java 的单继承特性而带来的局限
  2. 增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的
  3. 线程池只能放入实现 Runnable 或 Callable 类线程,不能直接放入继承 Thread 的类
    实现Runnable接口和实现Callable接口的区别
  4. Runnable 是自从 java1.1 就有了,而 Callable 是 1.5 之后才加上去
  5. 实现 Callable 接口的任务线程能返回执行结果,而实现 Runnable 接口的任务线程不能返回结果
  6. Callable 接口的 call()方法允许抛出异常,而 Runnable 接口的 run()方法的异常只能在内部消化,不能继续上抛
  7. 加入线程池运行,Runnable 使用 ExecutorService 的 execute 方法,Callable 使用 submit 方法 注:Callable 接口支持返回执行结果,此时需要调用 FutureTask.get()方法实现,此方法会阻塞主线程直到获
    取返回结果,当不调用此方法时,主线程不会阻塞

如何排除死锁

cmd命令排查

jps #查看当前线程
#14116 ThreadState
#找到你的class 对应的编号
jstack 14116 #线程的状态

idea排查

点击左侧相机
在这里插入图片描述

线程的状态

状态名称 说明
NEW 初始状态,线程被构建,但是还没调用start()方法
RUNNABLE 运行状态,Java线程在操作系统中就绪和运行两种状态笼统称作“运行中”
BLOCKED 阻塞状态,表示线程阻塞于锁
WAITING 等待状态,表示线程加入等待状态,进入该状态表示当前线程需要等待其它线程做出一定动作(通知或中断)
TIME_WAITING 超时等待状态,该状态不同于WAITING,它是可以在指定的时间自行返回的
TERMINATED 终止状态,表示当前线程已经执行完毕

线程的生命周期

在这里插入图片描述

线程的执行顺序

public class ThreadOrder {
    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            System.out.println("thread1");
        });
        Thread thread2 = new Thread(()->{
            System.out.println("thread2");
        });
        Thread thread3 = new Thread(()->{
            System.out.println("thread3");
        });
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

多个线程都是并行执行的,顺序随机,等待CPU调配
在这里插入图片描述

线程与线程池对比

现在有一个需求需要使用多线程已最快的速度先list容器插入1万个随机数

普通线程插入数据

既然要最快那我直接一口气开1万个线程,每一个线程只负责插入一个数组那不是一下子就完成了吗,但是效果并不是想象中那么块

public static void main(String[] args) throws InterruptedException {
    //记录开始时间
    Long start = System.currentTimeMillis();
    final List<Integer> list = new ArrayList<>();
    final Random random = new Random();
    //循环add随机数入容器
    for (int i=0;i<10000;i++){
        //每次插入开一个线程
        Thread thread = new Thread(){
            public void run(){list.add(random.nextInt());}
        };
        thread.start();
        //需要等待所有进程结束才能结束main进程不然会出现数据长度对不上
        thread.join();
    }
    System.out.println("耗时:"+(System.currentTimeMillis()-start));
    System.out.println(list.size());
}

线程池插入数据

public static void main(String[] args) throws InterruptedException {
    //记录开始时间
    Long start = System.currentTimeMillis();
    final List<Integer> list = new ArrayList<>();
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    final Random random = new Random();
    //循环add随机数入容器
    for (int i=0;i<10000;i++){
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                list.add(random.nextInt());
            }
        });
    }
    //停止接收新的任务,并且等待未完成任务完成后关闭线程池
    executorService.shutdown();
    //如果线程超时一天还没关闭,输出线程池没有关闭,直到关闭为止后执行后面代码
    while (!executorService.awaitTermination(1, TimeUnit.DAYS)) {
        System.out.println("线程池没有关闭");
    }
    System.out.println("线程池已经关闭");
    System.out.println("耗时:"+(System.currentTimeMillis()-start));
    System.out.println(list.size());
}

总结

new Thread:线程不是越多越好,如我们普通线程执行创建了1万个线程去add,但是还是比线程池要慢很多,其中原因在于,我们那1万个线程开启后,创建1万个对象,并且还要等待CUP调度,栈满后 java 每一次CG都要暂停所有线程,而且我们需要等待每一个线程join()完成后才会结束,一个线程join()另外在跑的线程都要挂起等待这个线程join()完成才能继续导致new Thread性能低下
Executor:线程池他重用存在的线程,减少对象创建、消亡的开销,提升性能。

线程池类图

在这里插入图片描述
其中一个线程池依赖关系,其余类似
在这里插入图片描述

生命周期

在这里插入图片描述
在ThreadPoolExecutor中有5个内部类,可分为2大类policy(策略) worker(工作)

//线程池使用案例
public static void main(String[] args) {
    //初始化一个线程池
    ExecutorService executorService = Executors.newCachedThreadPool();
    //调用submit方法 底层调用的是execute方法
    executorService.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
            System.out.println("CachedThreadPool 线程池");
        }
    });
    executorService.shutdown();
}

在这里插入图片描述

大概的流我们开始使用newCachedThreadPool初始化一个线程池,初始化后获取到一个ExecutorService并调用submit/execute方法,创建一个Work对象,使用addWorker入队,调用runWorker如何通过getTask从Worker队列中获取任务,调用run方法就是我们传入的业务代码启动任务,调用完成后会调用peocessWorkerExit方法判断看需不需要继续addWorker

execute在执行时其实会经过很多的判断,execute的大致流程如下

在这里插入图片描述

首先主函数会调用execute,会经过第一个判断判断一些核心容器还有没有位置,如果有位置就会去加入到核心容器使用addWork方法,核心容器会调用Work run=>runWork执行业务代码,如果核心容器满了就会offer(入队)到task中等到核心容器有空位和会从队列中出队到核心容器中,如果以上2个都满了会直接创建一个非核心的容器存着这些work,如果连非核心的容器也满了就会加入拒绝策略。

线程池运行思路

  • 如果当前池大小 poolSize 小于 corePoolSize ,则创建新线程执行任务
  • 如果当前池大小 poolSize 大于 corePoolSize ,且等待队列未满,则进入等待队列
  • 如果当前池大小 poolSize 大于 corePoolSize 且小于 maximumPoolSize ,且等待队列已满,则创建新线程执行任务
  • 如果当前池大小 poolSize 大于 corePoolSize 且大于 maximumPoolSize ,且等待队列已满,则调用拒绝策 略来处理该务
  • 线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,
    如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出

拒绝策略方案

  1. AbortPolicy:抛出异常,默认
  2. CallerRunsPolicy:不使用线程池执行
  3. DiscardPolicy:直接丢弃任务
    olSize 大于 corePoolSize 且大于 maximumPoolSize ,且等待队列已满,则调用拒绝策 略来处理该务
  • 线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,
    如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出

拒绝策略方案

  1. AbortPolicy:抛出异常,默认
  2. CallerRunsPolicy:不使用线程池执行
  3. DiscardPolicy:直接丢弃任务
  4. DiscardOldestPolicy:丢弃队列中最旧的任务对于线程池选择的拒绝策略可以通RejectedExecutionHandler handler = newThreadPoolExecutor.CallerRunsPolicy();来设置。
发布了33 篇原创文章 · 获赞 22 · 访问量 954

猜你喜欢

转载自blog.csdn.net/weixin_44642403/article/details/104138898