【JAVA今法修真】 第二章 一气化三清 线程分心念

发布于 2022年 01月 12日 10:45

这是我的微信公众号,希望有兴趣的朋友能够一起交流,也希望能够多多支持新人作者,你的每一份关注都是我写文章的动力:南橘ryc

天有八纪,地分九州,万法仙门与天道剑宗一并坐落在东北方通辽州。

与李小庚想象中的五步一楼,十步一阁,廊腰缦回,檐牙高啄,青鸾仙鹤飞舞,绕梁仙音奏响的场景不同,万法仙门就像上辈子的大学一样,分为教学区域,运动区域,科研区域,生活区域,办公区域。

被云小霄带到仙门人力长老那边简单检测了一下智商和骨龄,顺便放了一个侦测邪恶之后,李小庚就被登记为了一名万法仙门正式弟子。

修真不知日月,李小庚已经在万法仙门呆了半年了。

一日,万法仙门云霄殿中。

“小庚,你知道为什么我修炼比你快那么多倍吗?”云小霄很嚣张地打量着李小庚:“都半年了,你还没筑基成功,真是逊啊。”

“?”李小庚白了云小霄一眼:“谁不知道掌门是你爹?”

“虽然你说的没错,但是不知道你有没有在典籍里看过线程池神功?”

“你说的就是那个号称一气化三清,可以让人同时出现在很多地方的功法吗?”

“没错,多线程是我们万法仙门筑基期的必修科目,你目前修炼缓慢就是缺少了这一环的缘故,现在我就跟你好好的讲一讲。”


一、线程池的基础

介绍线程池之前要介绍线程,而介绍线程则离不开进程。

  • 进程:是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
  • 线程:就是进程中的一个独立控制单元,线程在控制着进程的执行。一个进程中至少有一个进程。
    而线程池,就是将多个线程进行池化处理,目前实际工作中很少会单独使用Thread来创建线程,而是使用线程池技术,根据业务实际创建线程。

1、线程池的相关属性:

  • corePoolSize(核心线程池大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。

  • maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。

  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。

  • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

  • workQueue任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。

    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程做些更有意义的事情,比如设置daemon和优先级等等

  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。

    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。

也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。


“哦,这么神奇,只需要设定好参数就能自动帮我修炼吗?”

“当然,不过在你还需要了解具体的运功路线才行,不然很容易出现内存溢出,经脉尽毁的问题,这一点很重要,你需要用心听”


2、线程池的执行流程

  • 1 如果运行的线程少于corePoolSize,则会添加新的线程,而不进行排队。
  • 2 如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
  • 3 如果无法将请求加入队列(队列已满),则创建新的线程,除非创建此线程超出 maximumPoolSize,如果超过,在这种情况下,新的任务将被拒绝。

“原来如此,在创建线程的时候,我需要设定好等待队列,同时把控住自己的内心,绝对不能因为贪图快速修炼就疯狂创建线程,不然一旦超过最大任务数量,又超过了队列,会出现不可预知的情况。”

“不愧是我看上的弟子!”云小霄摸了摸李小庚充满智慧的脑袋:“不过也没有那么危险,你可以使用一些策略来避免出现那些问题。据说这个技术就是不少修真界大拿在多次走火入魔之后总结出来的。”


3、线程池排队有三种通用策略

  1. 同步移交。队列的默认选项是同步移交,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。同步移交通常要求无界的maximumPoolSizes以避免拒绝新提交的任务。

但是,同步移交不适合管理资源的分配,除非特殊情况不推荐使用

  1. 无界队列。使用的是LinkedBlockingQueue类实现,不需要事先制定大小,也是按照“先进先出”算法处理任务。无界队列很好理解,就是和有界队列相反,使用无界队列的线程池,当有新任务提交时,如果线程池里有空闲线程,就分配线程立刻执行任务,否则就把任务放到无界任务队列中等待,如果线程池中一直没有空闲线程,但是新的任务又一直不停的提交上来,那么这些任务全部会被挂到等待队列中,一直到内存全部消耗完
  2. 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低CPU使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

我们最后可以这样理解:

  • 使用无界队列可能会耗尽系统资源
  • 使用有界队列可能不能很好的满足性能,需要调节线程数和队列大小
  • 线程数也有开销,所以需要根据不同应用进行调节

“原来如此,那我完全理解了。”

李小庚随手在空气中一划,空间仿佛遭遇了时空裂缝一般,撕开了一道口子。口子的另一边,有着无穷无尽半透明的李小庚,正排着歪歪扭扭的队伍,叽叽喳喳的从裂缝中出来。

“你就是我的玛斯特吗?”排头的李小庚对着一脸不可思议的云小霄挑了挑眉。

THREAD_POOL_CONFIG参数 = 李小庚自己设定的+∞
private static ThreadPoolTaskExecutor getThreadPoolTaskExecutor() {
    ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
    if (ObjectUtils.isNotEmpty(THREAD_POOL_CONFIG)) {
        //core thread count
        threadPoolTaskExecutor.setCorePoolSize(THREAD_POOL_CONFIG.getCorePoolSize());
        // max thread count
        threadPoolTaskExecutor.setMaxPoolSize(THREAD_PO0L_CONFIG.getMaxPoolSize());
        // queve capacity
        threadPoolTaskExecutor.setQueueCapacity(THREAD_POOL_CONFIG.getQueveCapacity());
        // thread name prefix
        threadPoolTaskExecutor.setThreadNamePrefix(THREAD_POOL_CONFIG.getThreadNamePrefixO);
        // thread await termination seconds
        threadPoolTaskExecutor.setAwaitTerminationSeconds(THREAD_POOL_CONFIG.getAwaitTerminationSeconds());
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}

“想死啊你!”云小霄剑光一闪。
poweroff

原本像黑洞一样愈发扩大的黑洞连着“李小庚”们,就像从来没有出现过一般。原本开始喧嚣了起来的云霄殿一瞬间回复了往日的平静。

整个大殿内还站着的只剩下云小霄,还有一只事不关己正在舔毛的银渐层。

云小霄撩起衣服上的流苏,探了探李小庚的鼻息,感觉到浑厚的呼吸之后,脸上的紧张马上化为了鄙夷:“呵呵,自作聪明。”,随手捏了一个法决 reboot -f

不多久,李小庚就像没事人一样的站了起来,随意的整了整发型,看见云小霄正一脸怒气的盯着他,连忙走过去给她捏起肩膀来:“师父,你看我刚刚屌吗?”

“呵呵,我再晚一步,就可以给你准备后事了。”云小霄享受着徒弟的服务,半年以来,她和李小庚的关系虽然说不上是父慈子孝,也算的上是父死子笑了,只是每次在搞完事情之后,李小庚总是能拿出一些新玩意。比如这个据说是来自于异界的正宗泰式马杀鸡,马上让长期进行伏案修真的云小霄舒服的眉开眼笑。

“师父,那你给我讲讲我的线程池存在漏洞的呗?”李小庚舔狗一样的在云小霄耳边道。

“线程池创建至今,也是经历过多代先贤的改进,从最开始的四大常用线程池,到现在现在各派独立发展的特异化线程池,只能说,没有不存在漏洞的线程池,只有最适合的线程池。”

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程若无可回收,则新建线程
  • newFixedThreadPool可以创建一个定长的线程池定长线程池最多只能同时执行一定个数的线程,这个容量在new的时候设定。
  • newScheduledThreadPool创建一个定时线程池支持延迟执行和周期性任务执行。后一种执行方式类似于单片机的定时器中断。
  • newSingleThreadExecutor创建一个单线程化的线程池,这个线程池当前池中的线程死后(或发生异常时),才能重新启动新的一个线程来替代原来的线程继续执行下去。也就是说按照单线程的模式,会按照线程添加的顺序,一个一个的执行这些线程的工作。

“那对于我这样虽然天赋异禀,但是修炼时间太短以至于法力匮乏的天才该如何选择线程池呢?”李小庚继续没脸没皮。

“当然,根据你自己的天赋还有修炼的功法不一样,肯定有不同的选择,这次认真给我听好了,下次再乱来就等着喝我洗脚水吧!”

“还有这等好事?咳咳...师父你说吧。”

二、设置线程池的大小

线程池的大小一直是大家很关心的问题,理想的大小取决于被提交任务的类型以及所部署的系统,代码中通常不会固定线程池的大小,而通过某种配置,或者Runtime.getRuntime().availableProcessors() 来动态计算。

Runtime.getRuntime().availableProcessors()这个代码大家可能不算太熟悉,这个方法可以获取CPU的数目。

  • 如果是CPU密集型应用,则线程池大小设置为N+1(或者是N),线程的应用场景:主要是复杂算法
  • 如果是IO密集型应用,则线程池大小设置为2N+1(或者是2N),线程的应用场景:主要是:数据库数据的交互,文件上传下载,网络数据传输等等。
    综合起来最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 +1)* CPU数目
    至于+1的原因,则是当线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费(剩余价值压榨的满满的)。
    当然,CPU并不是唯一影响线程池大小的资源,还应该考虑内存、文件句柄、套接字句柄、数据库连接等原因。

举个例子,比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为12,那么根据上面这个公式估算得到: ((0.5+1.5)/0.5+1)12=60

除了线程池大小上的显示设置以外,还可能由于其他资源上的约束而存在一些隐式限制,如应用程序使用一个包含10个连接的JDBC连接池,并且每个任务需要一个数据库连接,那么线程池就最好只有10个连接,因为当超过10个任务时,新的任务就需要其他任务释放连接。

“我明白了,其实线程池神功还是取决于我们的炼体功法,还有就是没有钱能不能用更大的处理法器。”李小庚一边说,手上的动作也加快起来,本来敷衍的按摩手法仿佛融入了穿越前体验的各位大师的经验,辅以修真人士的体魄,刹那间手臂上下飞舞,如梦如幻:“师父,你也知道我是村里来的,没啥钱,我看我们万法云推出了最新的套餐,只需要298块灵石,每个月就能享受8核16G的算力加持。”

“看你表现咯。”云小霄别过手来捏了捏李小庚的脸蛋,内心狂喜(这个徒弟好好看啊!):“当然,还有一些需要注意的地方,你需要记牢,使用线程池修炼虽然能加快速度,但是也会有很多危险的情况。”

三、避免线程池的饥饿死锁

1、线程的饥饿死锁

线程池中,如果任务依赖于其他任务,那么就有可能产生死锁。在单线程的Executor中,如果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交的任务的结果,那么就通常会产生死锁,这种情况被称为线程的饥饿死锁

对于线程池来说,只要池任务开始了无限期阻塞,例如某个任务的目的是等待一些资源或条件,但是只有另一个池任务的执行才能使那些条件成立。除非能保证线程池足够大,否则会发生线程饥饿死锁

下文清晰地展示了线程饥饿死锁的示例,队列为阻塞队列,因为线程池为是单线程的,当队列为空时,getHeader 将会一直阻塞等待 putHeader 执行。这就是任务之间相互依赖的饥饿死锁。

public class ThreadDeadlock {
    //创建一个队列、假设是存放头文件的地方
    private static BlockingQueue root = new ArrayBlockingQueue(10);
    public static void main(String[] args) {
        //创建一个固定线程的线程池
        ExecutorService service = Executors.newSingleThreadExecutor();
        service.submit(new getHeader());
        service.submit(new putHeader(1));
        service.shutdown();
    }
    static class putHeader implements Callable {
        private int val;
        public putHeader(int value) {
            val = value;
        }
        @Override
        public Object call() throws Exception {
            System.out.println("放置头文件");
            //往阻塞队列增加元素
            root.put(1);
            return "头文件";
        }
    }
    static class getHeader implements Callable {
        @Override
        public Object call() throws Exception {
            System.out.println("获取头文件");
            //取出阻塞队列的值,如果没有则会阻塞
            int value = (int) root.take();
            return "头文件";
        }
    }
}

2、运行时间较长的任务

如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得更糟。执行时间较长的任务不会造成线程池的堵塞,甚至还会增加执行时间较短任务的服务时间。如果线程池中的线程数量远小于在稳定状态下执行的任务的数量,那么到最后可能所有的线程都会运行这些执行时间较长的任务,从而影响整体的响应性。

可以通过限定任务等待资源的时间,不要去无限制地等待。在平台类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本,列如Thread.join、BlockingQueue.out、CountDownLatch.await以及Selector.select等。如果等待超时,那么可以把任务标识为失败,然后中止任务或者将任务重新放回队列。这样,无论任务的最终结果是否是成功,这种办法都能保证任务可以顺利执行而不会被阻塞住,并将线程释放出来执行一些能更快完成的任务。

当然了,如果线程池中总是充满了被阻塞的任务,也说明线程池设计的规模小了。

“小庚,这就是我们万法仙门《Java真经》中最重要的辅助手段之一线程池,之后如果遇上新的问题,我再跟你好好说一说吧,修真修真,便是修得真实。”

推荐文章