Java线程池


线程池

为什么要有线程池?

多线程是为了解决并发编程的方案,因为进程比较重量(创建销毁进程开销比较大),因此引入一个线程,线程比较进程要更加轻量。即便如此,在某些场景下,需要频繁大量的创建销毁线程时,线程的创建和销毁的开销就不能忽视了。此时Java就引入线程池。

使用线程池的好处

合理的使用线程池主要有三个好处:

  1. 降低资源的消耗:通过重复利用已经创建的线程来降低线程创建和销毁造成的消耗
  2. 提供程序响应速度:但又任务需要执行时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性:线程是稀缺的资源,如果无限制的创建,不但会消耗资源还会降低系统稳定性,使用线程池可以统一分配、调优和监控线程。

如果是真正创建或者销毁线程那就涉及到用户态和内核态的切换。

如果值是把线程放到线程池,就相当于全在用户态,在用户态效率就会更高。

用户态:应用程序执行的代码

内核态:操作系统执行的代码

ThreadPoolExecutor

ThreadPoolExecutor是Java标准库提供的线程池,它的构造方法的参数也是比较复杂的。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize:核心线程数

    • 表示线程池中的核心线程数量,也就是线程池保持的最小线程数
    • 核心线程会一直存活线程池中,即使它们处于空闲状态
    • 当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建新的线程,等到需要执行的任务数量大于线程池的基本大小时就不再创建。如果调用了线程池的 prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程
  • maximumPoolSize:最大线程数量(核心线程数+普通线程数)

    • 表示线程池中允许存在的最大线程数
    • 如果队列满了,并且以创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。需要注意的是,如果使用了无界的任务队列这个参数就没有什么效果了。
      如果我们要解决的任务场景任务量比较稳定,就可以设置 corePoolSize 和 maximumPoolSizejing 尽量接近一些(临时工就可以尽量少一些)
    • 相反如果要解决的任务场景,任务量波动比较大,就可以设置 corePoolSize 和 maximumPoolSize相差更大一些(临时工就可以多一些)这两者的值设置多少并不好说,要根据机器和任务场景通过实验的方式来确定。
  • keepAliveTime:普通线程空闲时间

    • 空闲后达到这个时间后就会被销毁释放资源
    • 这个keepAliveTime时间越短,其实就是越希望吃的资源能更少
  • unit:时间单位

    • keepAliveTime的时间单位(ms,s,minute)
    • 可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一秒)和纳秒(千分之一微秒)
  • workQueue:阻塞队列,存放任务

    • 可选一下阻塞队列

    • ArrayBlockingQueue:基于数组的有界阻塞队列

    • LinkedBlockingQueue:基于链表的阻塞队列,静态的工厂方法Executors.newFixedThreadPool ()使用了这个队列

    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,静态工厂方法 Executors.newCachedThreadPool 使用了这个队列。

    • PriorityBlockingQueue:一个具有优先级的无界阻塞队列。

  • threadFactory(线程工厂)

    • 用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置有意义的名字
  • handler(拒绝策略)

    • 当队列和线程池都满了,说明线程处于饱和状态,那么必须采取一种策略处理提交的新任务,这个策略默认是AbortPolicy,标准库中有以下拒绝策略

    • AbortPolicy:直接抛出异常

      • 默认的拒绝策略,当任务无法执行时,会立即抛出RejectedExecutionException异常
    • CallerRunsPolicy:只用调用者所在线程来运行任务

      • 当任务无法被执行时,会由提交任务的线程自己来执行任务
    • DiscardOldestPolicy:丢弃最旧策略

      • 丢弃队列里最近的一个任务,然后尝试将当前任务添加到工作队列
    • DiscardPolicy:不处理任务直接丢弃

      • 当前任务无法被执行时直接丢弃掉,不做其它操作
    • 也可以自定义拒绝策略,只需实现RejectedExecutionHandler接口,并实现其中的rejectedExecution()方法。以下是一个自定义拒绝策略的示例代码

      public class TestDemo6 implements RejectedExecutionHandler {
          @Override
          public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
              // 自定义拒绝策略
          }
      }
      

向线程池提交任务:在Java线程池中,execute方法和submit方法都可以用来提交任务,但它们有一些区别:

  1. 返回值:execute方法没有返回值,它只能提交没有返回值的任务。而submit方法返回一个Future对象,可以通过该对象判断任务是否执行成功可以通过ffuture的get()方法来获取返回值。
  2. 提交任务类型:execute方法只能接受实现了Runnable接口的任务对象。而submit方法既可以接受实现了Runnable接口的任务对象,也可以接受实现了Callable接口的任务对象
  3. 适用场景:execute方法适用于提交不需要任务返回值的任务。而submit方法适用于提交需要任务返回值的任务,并且可以通过返回的Future对象进行进一步操作

Executors

由于ThreadPoolExecutor使用起来比较复杂,标准库又提供了一组其他的类,相当于对ThreadPoolExecutor又进行了一层封装。

标准库中提供了一个 Executors这个类,这个类相当于一个“工厂类”,通过这个类提供的一组工厂方法,就可以创建出不同风格的线程池实例了。

public static void main(String[] args) {
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    threadPool.submit(new Runnable() {
        @Override
        public void run() {
            System.out.println("添加任务");
        }
    });
}
  • newFixedThreadPool: 创建固定线程数的线程池 (全是核心线程没有普通线程)
  • newCachedThreadPool: 创建线程数目动态增长的线程池 (完全没有核心线程,全是普通线程)
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.(在特定场景会使用)
  • newScheduledThreadPool: 能够设定延时时间的线程池(插入的任务能够过一会再执行). 相当于进阶版的Timer.
  • Executors就是ThreadPoolExecutor类的封装,通过工厂模式直接调用工厂方法来创建实例。

ThreadPoolExecutor 和 Executors 一个简单一个复杂,要根据任务场景来决定用哪个。简单问题用简单解法,复杂问题用复杂解法。

模拟实现简单版本线程池

模拟实现一个固定线程数量的线程池:

  • 需要一个阻塞队列来存放任务
  • 创建指定个线程线程从队列中获取任务执行
/**
 * 模拟实现简单版本线程池
 */
public class MyThreadPool {
    private int maxSizePool;
    private BlockingQueue<Runnable> blockingQueue;
    public MyThreadPool(int maxSizePool) {
        this.maxSizePool = maxSizePool;
        this.blockingQueue = new LinkedBlockingQueue<>();
        // 创建maxSizePool个线程从队列里去元素执行任务
        for (int i = 0; i < maxSizePool; i++) {
            Thread t = new Thread(()->{
               while (true) {
                   try {
                       Runnable task = blockingQueue.take();
                       task.run();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            });
            t.start();
        }
    }

    public void submit(Runnable task) {
        try {
            this.blockingQueue.put(task);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        MyThreadPool threadPool = new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            int tmp = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Thread: "+Thread.currentThread().getName()+",执行任务: "+tmp);
                }
            });
        }
    }
}

线程池的执行流程和线程数量设置

当向线程池提交一个新的任务

  1. 判断核心线程是否在执行任务,没满则创建一个新的工作线程来执行任务
  2. 如果核心线程已满,就判断任务队列是否已满,没满则将新提交的任务添加到工作队列中
  3. 如果队列已经满了,就判断整个线程池线程是否已满,如果没满则创建一个新的普通线程来执行该任务
  4. 若果线程池线程达到了最大数量,工作队列也满了,就会执行饱和(拒绝)策略(抛异常、丢弃最旧任务、由提交任务的线程自己执行任务、直接放弃该任务)

线程池的线程数量设置是要根据场景来设置的,如果机器的CPU占用比较大(做大量的运算和逻辑判断),设大了也没意义,因为此时的CPU占用是比较高的,如果机器基本都是在读写硬盘,CPU占用比较低,设大一点也没关系。所以线程池设置的线程数量是根据实际测试来进行设置的。

线程饥饿死锁

在线程池中,如果任务依赖于其他任务,那么可能就会产生死锁,比如一个任务将另一个任务提交到同一个线程池,并且在等待这个被提交任务的结果没那么通常会引发死锁。第二个任务停留在线程池的工作队列中,并等待第一个任务完成,而第一个任务又无法完成,因为他需要等待第二个任务的完成。比如:

  • 一个线程池中有多个工作线程
  • 某个任务A在执行过程中需要获取另一个任务B已经占用的资源
  • 任务B也在等线程池中的一个工作线程执行
  • 由于线程池中的工作线程有限,任务B的等待可能就会导致工作线程被全部占用,无法执行任务A
  • 这样任务A和任务B就导致了相互等待,就造成了线池死锁

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱敲代码的三毛

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

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

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

打赏作者

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

抵扣说明:

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

余额充值