线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处:
    1. 提高响应速度(减少了创建新线程的时间)
    2. 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    3. 便于线程管理(..)
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止

使用线程池

  • JDK 5.0起提供了线程池相关API: ExecutorService和Executors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    1. void execute(Runnable command)∶执行任务/命令,没有返回值,一般用来执行Runnable
    2. Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
    3. void shutdown():关闭连接池
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池

示例:我们使用Runnable来实现线程,并结合线程池来进行操作

public class ThreadPoll {
    public static void main(String[] args) {
        // 1. 创建服务,擦行间线程池
        // newFixedThreadPool(线程池大小)
        ExecutorService service = Executors.newFixedThreadPool(10);
        //执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        //关闭连接
        service.shutdown();
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

结果

pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
pool-1-thread-5
pool-1-thread-6

创建线程池的3大方法

  1. Executors.newSingleThreadExecutor(); 创建单个线程
  2. Executors.newFixedThreadPool(5); 创建一个固定的线程池的大小。
  3. Executors.newCachedThreadPool(); 缓存(可伸缩的)的线程池【遇强则强,遇弱则弱】

示例一 单个线程的线程池

public static void main(String args[]){
    ExecutorService threadPool = Executors.newSingleThreadExecutor();

    try {
        for (int i = 0; i < 5; i++) {
            //通过线程池创建线程
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName()+" ok");
            });
        }
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        //线程池用完必须要关闭线程池
        threadPool.shutdown();
    }
 }

结果

pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok

示例二 固定长度的线程池

public static void main(String args[]){
        ExecutorService threadPool = Executors.newFixedThreadPool(5);

        try {
            for (int i = 0; i < 5; i++) {
                //通过线程池创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            //线程池用完必须要关闭线程池
            threadPool.shutdown();
        }
    }

结果

pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-4 ok
pool-1-thread-5 ok

示例三 缓存线程池

  public static void main(String args[]){
        ExecutorService threadPool = Executors.newCachedThreadPool();

        try {
            for (int i = 0; i < 5; i++) {
                //通过线程池创建线程
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+" ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            //线程池用完必须要关闭线程池
            threadPool.shutdown();
        }
    }

结果: 如果for循环运行100次,不一定产生100条线程。这里之所以产生5条数据是因为cpu速率较快的原因。

pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-5 ok
pool-1-thread-3 ok

改变for循环的循环次数,多运行几次,你就可以体会缓存线程池”遇强则强,遇弱则弱“的含义。

线程池的7大参数

为什么要了解线程池的7大参数去自己定义线程池?我们来看看阿里巴巴规范就明白了。

在这里插入图片描述

我们来看看Executors创建线程池的源码

  • 第一个参数:核心线程数
  • 第二个参数:最大线程数
  • 第三个参数:活着的时间(如果超过这个时间,没有人调用就会被释放)
  • 第四个参数:第三个参数的时间单位
  • 第五个参数:阻塞队列
  • 第六个参数:创建线程的工厂,该参数一般不会改动
  • 第七个参数:拒绝策略

关于阻塞队列

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

//缓存线程池
 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
//固定大小的线程池
  public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
//通过上面的三种类型的线程池我们发现实际他们都掉眼泪ThreadPoolExecutor
//接下来我们来看看ThreadPoolExecutor的源码
public ThreadPoolExecutor(int corePoolSize,  //核心线程池大小
                          int maximumPoolSize, //最大的线程池大小
                          long keepAliveTime,  //超时了没有人调用就会释放
                          TimeUnit unit, //超时单位
                          BlockingQueue<Runnable> workQueue, //阻塞队列
                          ThreadFactory threadFactory, //线程工厂 创建线程的 一般不用动
                          RejectedExecutionHandler handler //拒绝策略
                         )

接下来我们举例一个银行业务来说明这些函数的具体含义

  • 首先有银行有5个柜台可以办理业务,这个5我们可以理解我最大线程数。
  • 平时银行只开放2个柜台来办理业务,这个2就表示核心线程。
  • 开放2个柜台办理业务,其他还没开始办理业务的人,在候客区进行等待(假设候客区有3个位置)。这个候客区就是阻塞队列。
  • 如果前两个柜台和候客区人都满了就会通知打开其它的柜台(开启其他的线程).
  • 如果所有客服业务都办理得差不多了,2,3,5在一定的等待时间内(超时等待)就会关闭(释放)。

自定义线程池

我们首先用默认的拒绝策略来写一个线程池,点击defaultHandler查看默认的拒绝策略。

理解默认决绝策略:当银行满了(柜台和候客区都满了)就不处理满了之后的人的业务,并且会抛出异常

private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

示例:

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolExample01 {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,
                5,
                3,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        try {
            for (int i = 1; i < 2; i++) {
                threadPoolExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }
    }
}

结果:改变for循环中i的判断条件进行多次测试的结果如下

//当for循环中i<2的时候结果如下
pool-1-thread-1 ok
//当for循环中i<=5的时候结果如下 核心线程数(平时开发的柜台)2+阻塞区(候客区)3=5 还是只使用两个柜台办理
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-1 ok
//当for循环中i<=2+3+3=8的时候就会触发最大线程池数。(开启其它柜台)
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-4 ok
//如果i>8(i<=9)的时候就会因为使用了默认的拒绝策略而抛出异常
java.util.concurrent.RejectedExecutionException:

4大拒绝策略

  1. new ThreadPoolExecutor.AbortPolicy;该拒绝策略为:银行满了,还有人进来,不处理这个人的,并抛出异常
  2. new ThreadPoolExecutor.CallerRunsPolicy();该拒绝策略为:哪来的去哪里, main线程进行处理
  3. new ThreadPoolExecutor.DiscardPolicy();该拒绝策略为:队列满了,丢掉其它多的人,不会抛出异常。
  4. new ThreadPoolExecutor.DiscardOldestPolicy(): 该拒绝策略为:队列满了,尝试去和最早的进程竞争,不会抛出异常

将上面自定义的线程池的决绝策略改为CallerRunsPolicy,for 中i<11,运行结果如下

pool-1-thread-1 ok
main ok
main ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-4 ok
pool-1-thread-3 ok
pool-1-thread-5 ok

将上面自定义的线程池的决绝策略改为DiscardPolicy,for 中i<11,运行结果如下(发现最多只执行8次)

pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-1 ok
pool-1-thread-3 ok
pool-1-thread-4 ok
pool-1-thread-5 ok

将上面自定义的线程池的决绝策略改为DiscardOldestPolicy,for 中i<11,运行结果如下

pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-2 ok
pool-1-thread-1 ok
pool-1-thread-1 ok
pool-1-thread-2 ok
pool-1-thread-3 ok
pool-1-thread-4 ok
pool-1-thread-5 ok

如何正确的设置线程池的最大线程数?

分两种情况考虑

  1. CPU 密集型 :为了提示cpu的效率,我们可以把本机电脑的cpu核数作为最大的线程数。获取CUP核数
  2. I/O密集型:IO操作是非常占用资源的。IO密集型就是判断程序中十分耗内存的操作IO线程数,假设我们有一个程序有15个大型IO任务,我们就可以设置最大线程数为IO任务的两倍约30个。
    参考课程:狂神学java
posted @ 2021-04-20 10:26  懒鑫人  阅读(71)  评论(0)    收藏  举报