【Java并发编程】对于Java线程池的一些研究

什么是线程池

线程池是Java中利用池化技术创建一组线程来进行管理和复用的机制,目的在于提高系统的性能和保护CPU资源不会被耗尽。

常见的线程池有哪些

固定线程数线程池-使用固定线程数线程池的话无论任务的多少线程池里面的线程数都是固定的

 ExecutorService executorService
    = Executors.newFixedThreadPool(10);

单线程线程池-单线程线程池里面的线程数只有一个 严格按照顺序来执行任务

ExecutorService singleThreadExecutor 
    = Executors.newSingleThreadExecutor();

定时任务处理线程池-专门用于处理定时任务的线程池

ExecutorService scheduledExecutorService 
    = Executors.newScheduledThreadPool(10);

线程池的核心构造参数有哪些

有七个参数分别是核心线程数、最大线程数、临时线程存活时间、临时线程存活时间单位、拒绝策略、线程工厂、任务队列

  • 核心线程数是线程池中始终保留的线程数量,当有任务过来以后首先交给核心线程数处理
  • 最大线程数是由临时线程数和核心线程数组合而成的,当任务队列满了以后就会创建临时线程来对任务进行处理了,但并不是一直创建的,创建的数量根据核心线程数和最大线程数两个参数决定
  • 临时线程存活时间、临时线程存活时间单位两个参数是搭配使用的,当临时线程在存活时间时间内并没有执行任务的话就会被销毁
  • 任务队列是用来存储任务的,当核心线程已经达到了核心线程数之后没有空闲的线程来处理任务时就会将任务存储到任务队列里面
  • 拒绝策略是当线程已经达到了最大线程数以后没有空闲的线程了这个时候就会执行相应的拒绝策略来拒绝执行
  • 线程工厂是用来创建线程实例的,对创建的线程实例提供了自定义的功能,比如说给线程池起名字,设置线程优先级,查看守护线程状态等等功能

如何重构线程工厂

线程工厂是用来创建线程实例的,对创建的线程实例提供了自定义的功能,比如说给线程池起名字,设置线程优先级,查看守护线程状态等等功能。在代码里面重构线程工厂的话可以自己创建一个类然后继承ThreadFactory接口然后重写newThread方法

public class TestThreadFactory implements ThreadFactory {
    private final AtomicInteger threadNumber;
    private final String namePrefix;
    private final boolean daemon;
    private final int priority;

    public TestThreadFactory(AtomicInteger threadNumber, String namePrefix, boolean daemon, int priority) {
        this.threadNumber = threadNumber;
        this.namePrefix = namePrefix;
        this.daemon = daemon;
        this.priority = priority;
    }

    @Override
    public Thread newThread(@NotNull Runnable r) {
        Thread thread = new Thread();
        //设置线程状态
        thread.setDaemon(daemon);
        //设置线程名称
        thread.setName(namePrefix + threadNumber.getAndIncrement());
        //设置优先级
        thread.setPriority(priority);
        return thread;
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 1,
                TimeUnit.MINUTES,
                new LinkedBlockingDeque<>(),
                new TestThreadFactory(new AtomicInteger(1),"TestThreadPool",false,5));
    }
}

拒绝策略有哪些?该如何选择

拒绝策略是当线程已经达到了最大线程数以后没有空闲的线程了这个时候就会执行相应的拒绝策略来拒绝执行。系统提供的拒绝策略分为四种

  • 直接报错
  • 直接丢弃被拒绝的任务
  • 哪个线程提交的拒绝任务就哪个线程执行
  • 丢弃最早提交的未处理任务然后尝试执行这个任务
  • 或者可以自定义拒绝策略
public class TestRejectHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println("当前任务数量已经达到了"+executor.getTaskCount()+"拒绝执行");
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 1,
                TimeUnit.MINUTES,
                new LinkedBlockingDeque<>(),
                new TestThreadFactory(new AtomicInteger(1),"TestThreadPool",false,5),
                new TestRejectHandler());
    }
}

对于拒绝策略的选择首先可以根据实际情况来选择

  • 如果是执行的任务都非常重要,对任务丢失零容忍的话可以选择默认的拒绝策略也就是抛出异常
  • 对于一些轻量级的任务或者耗时比较短的任务可以使用谁提交谁处理的方式作为拒绝策略
  • 对于优先级很低的任务比如说日志收集、不重要的数据统计任务这些可以容忍丢失的可以选择直接丢失新提交的任务
  • 对于要立刻处理最新数据并且优先级不太高的场景可以使用丢弃最早任务的拒绝策略,比如说需要数据处理系统中的定时刷新任务
  • 如果这些都不能满足的话就可以采用自定义拒绝策略来处理了

任务队列有哪些?该如何选择

对于任务队列来说分为有界队列、无界队列
有界队列就是说任务队列里面存放的任务数量是有限制的,而无界队列里面存放的任务数量是没有限制的
通常要选择有界队列避免任务持续堆积导致产生内存溢出的风险
对于无界队列来说Executor默认使用的就是无界队列

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

如果使用的是 ArrayBlockingQueue 的话就是有界队列了,对于 ArrayBlockingQueue 来说从源码就可以得知它是否加上公平锁

public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}

public ArrayBlockingQueue(int capacity, boolean fair) {
    if (capacity <= 0)
        throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull =  lock.newCondition();
}

临时线程存活时间该如何选择

临时线程的存活时间需要根据实际的场景来选择,如果处于一个高并发且执行任务时间比较长的场景,那么临时线程的存活时间就应该设置的长一点,避免设置的时间太短而导致频繁创建和销毁临时线程导致额外的消耗系统资源

Java线程池的工作流程

Java中的线程池的工作流程是:

  • 首先刚刚开始创建线程池的时候,线程池里面一个线程也没有直到调用executor方法提交任务过来才会去检查线程数量是否小于核心线程数,小于的话就会去创建核心线程执行任务
  • 之后当有任务提交过来交给如果当前的核心线程少于核心线程数,就创建核心线程进行处理
  • 如果当前的核心线程已经满了就会存入到任务队列中等待消费
  • 如果任务队列满了,就开启临时线程对任务进行处理,如果某个临时线程并没有在设置的存活时间内执行任务的话线程池会对其进行销毁
  • 如果当前的临时线程数+核心线程数超过了设置的最大线程数就根据设置拒绝策略执行相应的拒绝策略即可。

线程池的原理

在Java中线程池的原理实际上利用到了

  • 线程复用:对于线程来说,在执行完任务以后不会被立马销毁,如果是核心线程会一直处于就绪状态等待任务到来,如果是临时线程也是一样,只不过临时线程如果在设置的存活时间内没有执行任务的话就会被销毁。
  • 任务队列:由于创建和销毁线程是一个极其消耗CPU的事情,所以为了避免线程的频繁创建和销毁就利用到了任务队列来进行任务的堆积管理,等待核心线程进行处理。
  • 线程管理:线程池利用到了池化技术对线程进行动态管理,根据需要来创建线程。
  • 资源控制:为了避免线程过多导致CPU资源被消耗殆尽,使用线程池对线程进行控制从而达到CPU资源控制的效果。

线程池的好处有哪些

  • 合理分配资源:使用线程池的话首先是可以对线程进行可以合理的控制,合理分配资源,保护CPU资源不会被消耗殆尽。假如说有这样一个场景,后端有一个接口是查询数据库,其中接口里面有一个方法就是去开启线程去并发查询,这个方法里面使用的new一个Thread的方式创建线程,如果在同一时间有100个请求这个接口,那就会同时创建100个线程,CPU负载瞬间飙高可能会导致服务器直接崩溃。为了合理的控制线程数使用线程池来管理线程数,就可以合理的分配资源了。
  • 快速响应:使用线程池还可以更快的对任务进行响应处理,虽然当线程池刚刚创建的时候里面没有一个线程,但是在使用了以后,线程池里面会保留核心线程来等待任务,当一有任务过来就可以进行处理,相较于频繁的创建和销毁线程,这种方式响应速度更快。
  • 利于监控:不仅如此线程池里面也提供了线程工厂这个参数,通过重写线程工厂可以在打印日志的时候更好的追踪不同业务下的不同线程池的使用情况。

关于对线程池的关闭

对于shutdown来说线程池会立刻终止接受新的任务进来执行,但是对于还在执行的任务,线程池还是会继续执行的。

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

对于shutdownnow来说线程池不仅会立刻终止接受新的任务进来执行还会对于还在执行的任务也会尝试去终止执行并且返回一个还没有被执行的任务的队列。

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

所以从使用场景来说,shutdown适用于比较平滑的关闭线程池,对于shutdownnow来说适用于要尽快关闭线程池的场景.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值