关于java:知识总结Java线程池

39次阅读

共计 6034 个字符,预计需要花费 16 分钟才能阅读完成。

引出

获取多线程的办法,咱们都晓得有三种,还有一种是实现 Callable 接口

  • 实现 Runnable 接口
  • 实例化 Thread 类
  • 实现 Callable 接口
  • 应用线程池获取

Callable 接口

Runnable 和 Callable 的区别

  1. Runnable 接口没有返回值,Callable 接口有返回值
  2. Runnable 接口不会抛异样,Callable 接口能够抛异样
  3. 接口的办法不一样,一个 run 办法,一个 call 办法
  4. Callable 办法反对泛型

Callable 接口实现多线程

  • Callable 接口,是一种让线程执行实现后,可能返回后果的

当咱们实现 Ruannable 接口的时候,须要重写 run 办法,也就是 线程启动 的时候,会主动调用的办法。

同理,咱们实现 Callable 接口,也须要实现 call 办法,然而这个时候咱们还须要有返回值。这个 Callable 接口的利用场景个别在于 批处理业务,比方转账的时候,须要给返回后果的状态码回来,代表本次操作胜利还是失败。

public class MyThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {System.out.println("进入了 Callable 办法");
        return 1024;
    }
}

而后通过 Thread 线程,将 MyThread 实现 Callable 接口的类包装起来。

这里须要用到 FutureTask 类,他实现了 Runnable 接口,类的构造函数须要 传递一个实现 Callable 接口的类

public static void main(String[] args){
    // 1、实例化 FutureTask 类,传进去一个实现了 Callable 的类
    FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());

    // 2、而后在用 Thread 进行实例化,传入实现 Runnable 接口的 FutureTask 的类
    Thread t1 = new Thread(futureTask, "A 线程");
    t1.start();
    
    // 3、最初通过 futureTask.get() 获取到返回值
    System.out.println("FutureTask 的返回值为:" + futureTask.get());
}

原来咱们的形式是一个 main 办法冰糖葫芦似的串上来,引入 Callable 后,对于执行比拟久的线程,能够独自新开一个线程进行执行,最初再进行 汇总输入

如果咱们用 get() 获取 Callable 的计算结果,然而如果并没有计算实现,会导致阻塞,直到计算实现为止。也就是说,futureTask.get() 须要放在最初执行,这样不会导致主线程阻塞。

// 也能够应用上面算法,应用相似自旋锁的形式来进行判断是否运行结束
while(!futureTask.isDone()){}

留神

多个线程执行一个 FutureTask 对象的时候,只会计算一次。

FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());

// 开启两个线程计算 futureTask
new Thread(futureTask, "A 线程").start();
new Thread(futureTask, "B 线程").start();

如果咱们须要两个线程同时计算工作的话,那么须要定义两个 FutureTask 对象

FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread);
FutureTask<Integer> futureTask2 = new FutureTask<>(new MyThread);

// 开启两个线程计算 futureTask
new Thread(futureTask1, "A 线程").start();
new Thread(futureTask2, "B 线程").start();

ThreadPoolExecutor

为什么要用线程池?

线程池做的次要工作就是 管制运行的线程的数量,处理过程中,将工作放入到阻塞队列中,而后线程创立后,启动这些工作,如果线程数量超过了最大数量的线程排队等待,等其它线程执行结束,再从队列中取出工作来执行。

它的次要特点为:线程复用、管制最大并发数、治理线程

线程池的益处

  1. 升高资源耗费。通过反复利用已创立的线程,升高线程创立和销毁造成的耗费。
  2. 进步响应速度。当工作达到时,工作能够不须要等到线程创立就立刻执行。
  3. 进步线程的可管理性。应用线程池能够进行对立的调配,调优和监控。

线程池的实现原理

说白了就是一个 线程汇合 (workerSet)和一个 阻塞队列 (workQueue)。当用户向线程池提交一个 工作 时,线程池会先将工作放入阻塞队列中。线程汇合中的线程会一直的从阻塞队列中获取工作执行,当阻塞队列中没有工作的时候,就会阻塞,直到队列中有工作了就取出来继续执行。

工作:客户。线程汇合:办理窗口。阻塞队列:候客区。

架构阐明

Java 中线程池是通过 Executor 框架实现的,该框架中用到了 ExecutorExecutors(辅助工具类),ExecutorServiceThreadPoolExecutor 这几个类。

创立线程池

  • 咱们通过 Executors 工具类来创立线程池
1、FixedThreadPool(固定线程的线程池)
ExecutorService threadPool = Executors.newFixedThreadPool(5);
  1. 执行长期的工作,性能好很多
  2. 可控制线程数 最大并发数,超出的线程会在队列中期待
  3. 应用场景:执行长期的工作
2、SingleThreadExecutor(一个线程的 单线程池)
ExecutorService threadPool = Executors.newSingleThreadExecutor();
  1. 一个工作一个工作执行的场景
  2. 用惟一的工作线程来执行工作,保障所有工作依照指定程序执行
  3. 执行场景:一个工作一个工作执行的场景
3、newCacheThreadPool(可扩容的线程池)
ExecutorService threadPool = Executors.newCacheThreadPool();
  1. 执行很多短期异步的小程序或者负载较轻的服务器
  2. 线程长度超过解决须要,可灵便回收闲暇线程,如无可回收,则新建新线程
  3. 执行很多短期异步的小程序或者负载较轻的服务器
代码演示
  • 模仿 10 个用户来办理业务,每个用户就是一个来自内部申请线程
ExecutorService threadPool = Executors.newFixedThreadPool(5);
    // 模仿 10 个用户来办理业务,每个用户就是一个来自内部申请线程
    try {
        // 循环十次,模仿业务办理,让 5 个线程解决这 10 个申请
        for (int i = 0; i < 10; i++) {
            final int tempInt = i;

            threadPool.execute(() -> {System.out.println(Thread.currentThread().getName() + "给用户:" + tempInt + "办理业务");
            });
        }

    } catch (Exception e) {e.printStackTrace();
    } finally {threadPool.shutdown(); // 用池化技术,肯定要记得敞开
    }

后果:

咱们可能看到,一共有 5 个线程,在给 10 个用户办理业务。

底层实现

咱们通过查看源码,发现底层都是应用了ThreadPoolExecutor

为什么单线程池和固定线程池应用的工作阻塞队列是 LinkedBlockingQueue(),而缓存线程池应用的是 SynchronousQueue()呢?

因为在单线程池和固定线程池中,线程数量是无限的,因而提交的工作须要在队列中期待闲暇的线程。

而在缓存线程池中,线程数量简直有限,因而提交的工作在 SynchronousQueue 队列中同步给空余线程即可。

Tips:SynchronousQueue 是一个没有存储空间的队列,生产者进行 put 操作时,必须要期待消费者的 take 操作。

线程池的重要参数

线程池在创立的时候,一共有 7 大参数:

  • corePoolSize:外围线程数,线程池中的常驻外围线程数
  • maximumPoolSize:线程池可能包容同时执行的最大线程数,此值必须大于等于 1
  • keepAliveTime:多余的闲暇线程存活工夫
  • unit:keepAliveTime 的单位
  • workQueue:工作队列,被提交的但未被执行的工作(相似于银行外面的候客区)

    • LinkedBlockingQueue:链表阻塞队列
    • SynchronousBlockingQueue:同步阻塞队列
  • threadFactory:示意生成线程池中工作线程的线程工厂,用于创立线程池 个别用 默认即可
  • handler:回绝策略,示意当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize)时,如何来拒绝请求执行的 Runnable 的策略

线程池底层工作原理

整个线程池的工作就像 银行办理业务 一样。

  1. 最开始假如来了两个顾客,因为 corePoolSize 为 2,因而这两个顾客间接可能去窗口办理。
  2. 前面又来了三个顾客,因为 corePool 曾经被顾客占用了,因而只有去候客区,也就是阻塞队列中期待
  3. 前面人越来越多,候客区可能不够用了,这时须要申请减少解决申请的窗口,如果 maximumPoolSize 为 5,就会创立这 3 个非核心线程运行这个工作。
  4. 假如受理窗口曾经达到最大数,并且申请数还是一直递增,此时候客区和线程池都曾经满了,为了避免大量申请冲垮线程池,曾经须要开启回绝策略
  5. 长期减少的线程如果超过了最大存活工夫,就会销毁,最初从最大数缩容到外围线程数

ps:长期减少的业务窗口,会先解决那些前面来的,没地位坐的客户。(候客区客户 os:凭什么 = =)

四种回绝策略的解析

以下所有回绝策略都实现了 RejectedExecutionHandler 接口

  • AbortPolicy(默认):间接抛出 RejectedExcutionException 异样,阻止零碎失常运行
  • DiscardPolicy:间接抛弃工作,不予任何解决也不抛出异样,如果运行工作失落,这是一种好计划
  • CallerRunsPolicy:用调用者所在的线程解决工作,可能减缓新工作的提交速度。
  • DiscardOldestPolicy:抛弃最老的工作,执行当前任务。

为什么不必默认创立的线程池?

工作中应该用哪一个办法来创立线程池呢?答案是 一个都不必,咱们生产上只能应用自定义的。

阿里巴巴开发手册:【强制】线程池不容许应用 Executors 去创立,而是通过 ThreadPoolExecutor 的形式,这样的解决形式让写的同学更加明确线程池的运行规定,躲避资源耗尽的危险。

弊病如下:

  1. FixedThreadPool 和 SingleThreadPool 应用了无界队列(LinkedBlockingQueue),可能会沉积大量的申请,从而导致 OOM
  2. CacheThreadPool 和 ScheduledThreadPool 的最大线程为 Integer.MAX_VALUE,可能会创立大量的线程,从而导致 OOM

手写线程池

采纳默认回绝策略(AbortPolicy)
ExecutorService threadPool = new ThreadPoolExecutor(
                                2, 
                                5, 
                                1L, 
                                TimeUnit.SECONDS,
                                new LinkedBlockingQueue<Runnable>(3),// 候客区 3 个座位
                                Executors.defaultThreadFactory(),
                                new ThreadPoolExecutor.AbortPolicy());
// 而后应用 for 循环,模仿 10 个用户来进行申请
for (int i = 0; i < 10; i++) {
    
    final int tempInt = i;
    threadPool.execute(() -> {System.out.println(Thread.currentThread().getName()+"给用户:"+tempInt+"办理业务");
    });
    
}

然而用户执行到第九个的时候,触发了异样,程序中断。

这是因为触发了 AbortPolicy 的回绝策略:间接报异样。

触发条件是,申请的线程大于 阻塞队列大小 + 最大线程数 = 8 的时候,也就是说第 9 个线程来获取线程池中的线程时,就会抛出异样。

采纳 CallerRunsPolicy 回绝策略

也称为回退策略,就是用调用者所在的线程解决工作。

咱们看运行后果:

咱们发现,输入的后果外面呈现了 main 线程,因为线程池触发了回绝策略,把工作回退到 main 线程,而后 main 线程对工作进行解决。

DiscardPolicy 回绝策略、DiscardOldestPolicy 回绝策略

这两种策略都是把工作抛弃。

前者抛弃的是,进来排队排不上的工作。

后者抛弃的是以后队列中最老的工作,即排队下一个就到你了,然而因为有人进来,导致你被抛弃了(为什么这么惨?)。解决逻辑如下:

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {if (!e.isShutdown()) {e.getQueue().poll();        // 用 poll() 移除队列中的首个元素
     e.execute(r);                // 执行当前任务
 }
}

线程池的正当参数

生产环境中如何配置 corePoolSize 和 maximumPoolSize?。这个是依据具体业务来配置的,分为 CPU 密集型和 IO 密集型。

// 能够看 CPU 是几核的
Runtime.getRuntime().availableProcessors();
  • CPU 密集型

CPU 密集型也叫计算密集型,指的是零碎的硬盘、内存性能比 CPU 要好,CPU 的 IO 操作很快,然而 CPU 还有很多运算要解决,导致系统的 CPU 大部分都是 100%。

须要尽可能少的线程数量,个别为:CPU 核数 + 1

  • IO 密集型

即该工作须要大量的 IO,即大量的阻塞。IO 包含:数据库交互,文件上传下载,网络传输等

IO 密集型指的是零碎的 CPU 性能绝对硬盘、内存要好很多。此时大部分的情况是 CPU 在等 IO 操作,则应配置尽可能多的线程,如 CPU 核数 * 2

参考公式:CPU 核数 / (1 – 阻塞零碎)。

个别阻塞系数是 0.9,比方 8 核 CPU,应该配置 8 / 0.1 = 80 个线程数

一个线程池中的线程异样了,那么线程池会怎么解决这个线程?

如果执行形式是 execute 时,会看到堆栈异样的输入。

当执行形式是 submit 时,堆栈异样没有输入。并且调用 Future.get() 办法时,能够捕捉到异样。不会影响线程池外面其余线程的失常执行,线程池会把这个异样的线程移除掉,并创立一个新的线程放入线程池中。?

正文完
 0