并发编程笔记(3)

池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

这里借用《Java 并发编程的艺术》提到的来说一下使用线程池的好处:

    1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

    2.提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

3.提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2、自定义线程池

1.自定义拒绝策略接口

2.自定义任务队列

步骤3:自定义线程池

步骤4:测试

3、ThreadPoolExecutor

1)线程池状态

ThreadPoolExecutor 使用 int 的 3 位来表示线程池状态,低 29 位表示线程数量

从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING

注意:最高位代表符号位

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

2)构造方法

构造参数解释:

    1.corePoolSize:核心线程数

    2.maximumPoolSize:最大线程数

        maximumPoolSize - corePoolSize = 救急线程数

    3.keepAliveTime:救急线程空闲时的最大生存时间

    4.unit:时间单位

    5.workQueue:阻塞队列(存放任务)

        有界阻塞队列 ArrayBlockingQueue

        无界阻塞队列 LinkedBlockingQueue

        最多只有一个同步元素的队列 SynchronousQueue

        优先队列 PriorityBlockingQueue

    6.threadFactory:线程工厂(给线程取名字)

    7.handler:拒绝策略

工作方式:

1.线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。

2.当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入 workQueue 队列排 队,直到有空闲的线程。

3.如果队列选择有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线 程来救急。

4.如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 下面的前 4 种实现,其它著名框架也提供了实现

    1) ThreadPoolExecutor.AbortPolicy 让调用者抛出RejectedExecutionException 异常,这是默认策略

    2) ThreadPoolExecutor.CallerRunsPolicy 让调用者运行任务

    3) ThreadPoolExecutor.DiscardPolicy 放弃本次任务

    4) ThreadPoolExecutor.DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之

    5)Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方 便定位问题

    6)Netty 的实现,是创建一个新线程来执行任务

    7)ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略

    8)PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略

5.当高峰过去后,超过 corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。

3)newFixedThreadPool

这个是 Executors 类提供的静态的工厂方法来创建线程池!Executors 是 Executor 框架的工具类,newFixedThreadPool 创建的是固定大小的线程池。实现代码如下:

特点:

    1.核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间

    2.阻塞队列是无界的(LinkedBlockingQueue),可以放任意数量的任务

    3.适用于任务量已知,相对耗时的任务

4)newCachedThreadPool

特点

   1. 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着

        全部都是救急线程(60s 后没有任务就回收)

        救急线程可以无限创建

    队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交 货)SynchronousQueue,底层利用了Transfer

    整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。

    适合任务数比较密集,但每个任务执行时间较短的情况

5)newSingleThreadExecutor

使用场景:

希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

    和自己创建单线程执行任务的区别:自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作

    Executors.newSingleThreadExecutor() 线程个数始终为 1 ,不能修改

    FinalizableDelegatedExecutorService 应用的是装饰器模式只对外暴露了 ExecutorService 接口,因 此不能调用 ThreadPoolExecutor 中特有的方法

    和Executors.newFixedThreadPool(1) 初始时为1时的区别:Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改,对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改

注意,Executors 返回线程池对象的弊端如下:

FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

6)提交任务

7)关闭线程池

异步模式之工作线程

定义:
让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。

例如:

海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那 么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message) 注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率

例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成 服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工。

饥饿:

固定大小线程池会有饥饿现象

(因为任务是两阶段的,存在一个线程需要另外一个线程的执行结果的情况)

    两个工人是同一个线程池中的两个线程

    他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作

        1.客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待

        2.后厨做菜:没啥说的,做就是了

    比如工人 A 处理了点餐任务,接下来它要等着 工人 B 把菜做好,然后上菜,他俩也配合的蛮好 但现在同时来了两个客人,这个时候工人 A 和工人 B 都去处理点餐了,这时没人做饭了,这就是饥饿。

解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,

解决方法:不同的任务类型,采用不同的线程池。实现代码如下:

创建多大的线程池合适?

过小会导致程序不能充分地利用系统资源、容易导致饥饿,过大会导致更多的线程上下文切换,占用更多内存,

    CPU 密集型运算 通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

    I/O 密集型运算 CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程 RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。

        经验公式如下:

线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

        例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式 4 * 100% * 100% / 50% = 8

        例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式 4 * 100% * 100% / 10% = 40

8)任务调度线程池

在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但 由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个 任务的延迟或异常都将会影响到之后的任务。

使用 ScheduledExecutorService 改写:

    整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务。

    ScheduledExecutorService 中 scheduleAtFixedRate 方法的使用,以固定的速率执行任务 ,不会发生任务的重叠

    ScheduledExecutorService 中 scheduleWithFixedDelay 方法的使用,从上一次任务结束后,以固定的delay去进行下一次任务执行。

9)正确处理执行任务异常

可以发现,如果线程池中的线程执行任务时,如果任务抛出了异常,默认是中断执行该任务而不是抛出异常或者打印异常信息。
 

方法1:主动捉异常

方法2:使用 Future,错误信息都被封装进submit方法的返回值中

应用之定时任务

如何让每周四 18:00:00 定时执行任务?

10)Tomcat 线程池

1/LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲

2.Acceptor 只负责【接收新的 socket 连接】

3.Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】

4.一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理

5.Executor 线程池中的工作线程最终负责【处理请求】

Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同,如果总线程数达到 maximumPoolSize,这时不会立刻抛 RejectedExecutionException 异常,而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常。

源码 tomcat-7.0.42

4、Fork/Join

1) 概念

Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率Fork/Join 默认会创建与 cpu 核心数大小相同的线程池

2) 使用

提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务

改进:修改任务的拆分方式

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值