java线程池

 线程池是生产者消费者模式,它解决的问题是协调生产者和消费者的速率,同时避免线程切换,提升运行速度。

下面是我对线程池的一些理解,主要原理图如下:

一、java线程池原理

参考如下文章

手写线程池(简化版)-CSDN博客

核心一:死循环获取任务执行

初始化一些线程,它们一直执行一个while死循环方法,在该方法中,不停地去blockingqueue中获取任务,然后执行,如下:

    private void runWorker(Worker worker) {
        if(worker==null) throw new NullPointerException("空的工作线程");
        try {
            Runnable firstTask = worker.firstTask;
            Thread wk = worker.thread;
            worker.firstTask = null;
            // 当前的任务先给他完成了
            while(firstTask!=null || (firstTask=getTask())!=null){
                // 如果执行线程池的stop方法,所有工作队里都要进行interrupted
                if(wk.isInterrupted()){
                    System.out.println("this thread is interrupted");
                }
                if(status.get()==STOP){
                    System.out.println("this threadPoll has already stopped");
                }

                firstTask.run();
                firstTask = null;
                worker.finishTaskCount++;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            // 没任务了退出
            while (true){
                // 原子类的操作有可能失败,所以要不停重试,直到成功为止
                if(casDeleteWorkerCount()){
                    finishedTaskCount += worker.finishTaskCount;
                    break;
                }else{
                    continue;
                }
            }
            L.lock();
            try {
                workers.remove(worker);
            } finally {
                L.unlock();
            }
        }
    }

核心二:阻塞队列协调生成者消费者速率

如果一直死循环下去,对cpu是比较大的浪费,blockingqueue可以让线程在没有任务时,阻塞停止获取任务,当有任务时再唤醒线程。同时插入任务时,队列满了也能执行拒绝策略,阻断生成者。

二、blockingqueue原理

线程池主要核心是阻塞队列,下面再看下阻塞队列的原理:

blockingqueue是通过加Reentrantlock锁的方式实现的添加元素时,添加元素时,先获取锁,然后判断队列是否为空,如果为空调用condition.wait()方法,将线程阻塞,如下:

    /**
     * 从队列中取一个元素
     * @return [description]
     * @throws InterruptedException [description]
     */
    public E take() throws InterruptedException {
        //对操作进行加锁 多线程时轮流取元素
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            //如果队列中没有对象 则阻塞线程等待
            while (count == 0)
                //重点:等待存数据的线程通知
                notEmpty.await();
            //代码运行到此处说明count!=null 执行从队列中取元素
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

而condition.wait()是aqs中的方法,它将当前线程封装成一个节点,插入到aqs中,下面再看下aqs原理。

三、aqs原理

aqs是抽象队列同步器,顾名思义,它是用来进行线程间同步的,也就是加锁,然后它是使用一个state变量和一个队列来实现的。获取锁时,先使用cas来设置state变量,设置成功代表获取到了锁,设置失败则将当前线程包装成队列的一个节点,插入队列中,并且使用LockSupport.park方法阻塞当前线程。如下:

/**
 * 让线程不间断地获取锁,若线程对应的节点不是头节点的下一个节点,则会进入等待状态
 * @param node the node
 */
final boolean acquireQueued(final Node node, int arg) {
    // 记录失败标志
    boolean failed = true;
    try {
        // 记录中断标志,初始为true
        boolean interrupted = false;
        // 循环执行,因为线程在被唤醒后,可能再次获取锁失败,需要重写进入等待
        for (;;) {
            // 获取当前线程节点的前一个节点
            final Node p = node.predecessor();
            // 若前一个节点是头节点,则tryAcquire尝试获取锁,若获取成功,则执行if中的代码
            if (p == head && tryAcquire(arg)) {
                // 将当前节点设置为头节点
                setHead(node);
                // 将原来的头节点移出同步队列
                p.next = null; // help GC
                // 失败标志置为false
                failed = false;
                // 返回中断标志,acquire方法可以根据返回的中断标志,判断当前线程是否被中断
                return interrupted;
            }
            // shouldParkAfterFailedAcquire方法判断当前线程是否能够进入等待状态,
            // 若当前线程的节点不是头节点的下一个节点,则需要进入等待状态,
            // 在此方法内部,当前线程会找到它的前驱节点中,第一个还在正常等待或执行的节点,
            // 让其作为自己的直接前驱,然后在需要时将自己唤醒(因为其中有些线程可能被中断),
            // 若找到,则返回true,表示自己可以进入等待状态了;
            // 则继续调用parkAndCheckInterrupt方法,当前线程在这个方法中等待,
            // 直到被其他线程唤醒,或者被中断后返回,返回时将返回一个boolean值,
            // 表示这个线程是否被中断,若为true,则将执行下面一行代码,将中断标志置为true
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 上面代码中只有一个return语句,且return的前一句就是failed = false;
        // 所以只有当异常发生时,failed才会保持true的状态运行到此处;
        // 异常可能是线程被中断,也可能是其他方法中的异常,
        // 比如我们自己实现的tryAcquire方法
        // 此时将取消线程获取锁的动作,将它从同步队列中移除
        if (failed)
            cancelAcquire(node);
    }
}

其中cas是比较并替换,由cpu包装这条指令执行的线程安全性。LockSupport.park方法底层是c语言写的方法。

四、线程阻塞

aqs主要是可以阻塞当前线程,那线程阻塞是什么意思呢?

线程和进程其实差不多,linux内核中把线程也当做进程来处理。学习c语言代码时,首先会写个main函数,代表一个程序的进程,所以线程其实就是一段函数,它存储在内存中,主要包含线程的堆栈,代码,当前执行到哪一行和一些数据,还有描述线程的一些状态信息等等。线程的阻塞其实就是改变当前线程的状态,使得cpu不执行当前线程。LockSupport.park是将当前线程的状态变成等待。

五、同类框架

线程池是使用加锁+线程阻塞的方式解决生产者消费者问题的,那有其他方式吗?lmax公司开发了一款disruptor框架,采用无锁的方式来实现,效率比线程池高很多。

disruptor是使用一个环形队列来存储任务,生产者存的时候,首先使用cas获取队列中可以写的位置,然后写队列,这里就是无锁的操作。消费者读队列的时候,首先查询available queue中的位置,获取可以读的位置,然后进行读操作。

消费者没有消息时的等待策略:

BlockingWaitStrategy(阻塞等待策略)SleepingWaitStrategy(休眠等待策略)YieldingWaitStrategy(让出等待策略)BusySpinWaitStrategy(忙等待策略)

具体实例参考:并发框架——Distruptor_disruptor-CSDN博客

六、实际应用

1、web服务器接收请求时,使用线程池,可以避免线程创建过多,拖垮服务器

2、消费mq消息时,使用线程池可以加快消息的消费

3、日志打印时,使用线程池可以异步打印,提高接口性能

4、excel文件导入时,可以使用线程池异步导入,返回给用户导入中的状态,前端再不停地请求状态

5、接口中包含多个第三方接口请求,可以用线程池并行执行,减少用户等待

6、执行大量耗时的操作,比如文件下载,可以使用线程池分段并行下载

7、连接池也是使用线程池,每个线程保存连接,减少连接创建的时间

......

七、线程池使用问题

1、threadLocal存储用户信息,在线程池中不能传递,导致获取失败

解决方法:使用TransimittableThreadLocal,GitHub - alibaba/transmittable-thread-local: 📌 a missing Java std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.

2、主线程和子线程使用同一线程池执行,造成线程池死锁

解决方法:不同业务分开定义线程池

3、生产者的速率比消费者快,导致线程池满,执行拒绝策略

解决方法:可以使用SynchronousQueue,让调用者自己执行

4、线程池内抛出异常没有try catch

如果是调用execute方法执行任务,抛出异常到外层,当前线程结束,添加一个新的工作线程

如果是调用submit方法执行任务,不抛异常,调用get方法获取结果时才抛异常,当前线程变成wait状态

如果是ScheduledThreadPoolExecutor,抛出异常,定时任务终止

解决方法:线程池内的任务需要加try catch

5、线程池核心线程数量设置

cpu密集型cpu核数+1,io密集型cpu核数*2+1,这里理论值,实际生产中还需要通过接口压测的方式来确定线程数量

  • 24
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值