七、 共享模型之工具(主要是线程池)
1、自定义线程池
概念
阻塞队列中维护了由主线程(或者其他线程)所产生的的任务
主线程类似于生产者,产生任务并放入阻塞队列中
线程池类似于消费者,得到阻塞队列中已有的任务并执行
实现了一个简单的线程池的思路
1. 阻塞队列BlockingQueue用于暂存来不及被线程执行的任务
- 也可以说是平衡生产者和消费者执行速度上的差异
- 里面的获取任务和放入任务用到了生产者消费者模式
- 线程池中对线程Thread进行了再次的封装,封装为了Worker
- 在调用任务的run方法时,线程会去执行该任务,执行完毕后还会到阻塞队列中获取新任务来执行
- 线程池中执行任务的主要方法为execute方法
- 执行时要判断正在执行的线程数是否大于了线程池容量
或者
步骤1:自定义拒绝策略接口
步骤2:自定义任务队列
阻塞队列类
成员变量:任务队列,锁,生产者条件,消费者条件,容量
成员方法:阻塞添加,阻塞获取,带超时阻塞添加,带超时阻塞获取,数量,带策略阻塞添加
步骤3:自定义线程池
线程池类
构造方法:
成员变量:任务队列,线程集合,核心线程数,获取任务的超时时间,拒绝策略
成员方法:执行任务
线程类(worker)继承
成员变量: Runnable
成员方法:run
步骤4:测试
自定义线程池类
多个线程执行
2、ThreadPoolExecutor
(1)线程池的状态
(2)构造方法
工作方式:当一个任务传给线程池以后,可能有以下几种可能
- 将任务分配给一个核心线程来执行
- 核心线程都在执行任务,将任务放到阻塞队列workQueue中等待被执行
- 阻塞队列满了,使用救急线程来执行任务。救急线程用完以后,超过生存时间(keepAliveTime)后会被释放
- 任务总数大于了 最大线程数(maximumPoolSize)与阻塞队列容量的最大值(workQueue.capacity),使用拒接策略
拒绝策略:如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略
- AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
- CallerRunsPolicy 让调用者运行任务
- DiscardPolicy 放弃本次任务
- DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
另外,其他的实现方式
- Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方 便定位问题
- Netty 的实现,是创建一个新线程来执行任务
- ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
- PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由 keepAliveTime 和 unit 来控制。
(3)newFixedThreadPool
特点
核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
阻塞队列是无界的,可以放任意数量的任务
评价:适用于任务量已知,相对耗时的任务
(4)newCachedThreadPool
特点
核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着 全部都是救急线程(60s 后可以回收);救急线程可以无限创建
队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)
评价:整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况
(5)newSingleThreadPool
使用场景:
希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
区别:
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作。
- Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改。inalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法。
- Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改。对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
(6)提交任务
(7)关闭线程池
3、设计模式 (异步模式之工作线程)
让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为分工模式,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。
工作线程本质上是分工。不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率。例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工。
4、Timer 相关 (quarts)
Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
5、ScheduledThreadPoolExecutor
scheduleAtFixedRate 例子(任务执行时间超过了间隔时间),由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s
scheduleWithFixedDelay 例子。这个不会产生间隔效果。
6、处理异常
方法1:主动捉异常
方法2:使用 Future
7、Tomcat线程池
8、Fork/Join
八、锁相关 ( AQS 和ReentrantLock)
1、AQS概述
(1)定义
(2)实现自定义锁
自定义同步器extends AbstractQueuedSynchronizer
(tryAcqurie, tryRelease, condition)
自定义锁,有了自定义同步器,很容易复用 AQS ,实现一个功能完备的自定义锁
(3)理解
AQS 要实现的功能目标
阻塞机制,阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire
获取锁超时机制
通过打断取消机制
独占机制及共享机制
条件不满足时的等待机制
2、Reentrantlock
简单说
加锁过程
成功,结束。
失败,进入队列。
尝试,2次后,没有获得(有公平和非公平),挂起(线程状态为-1)。
线程释放锁的过程
如果没有其他线程抢,等待队列中可以获得。
如果有其他线程抢,并且成功。
公平和非公平的区别:
- 公平锁
想要获取锁资源的线程在队列里进行排队等待,新来的线程去队尾排队,锁资源释放的时候只有队首线程可以获得锁资源。
- 非公平锁
新来的线程直接和队首线程争抢锁资源,如果争抢到了,则直接获取锁资源,队首线程继续等待。 如果新来的线程竞争失败,则去队尾进行排队,只能等待队列前所有线程执行完毕后自己才能获取锁。
注意,公平锁与非公平锁主要区别在于 tryAcquire 方法的实现;公平锁会先检查 AQS 队列中是否有前驱节点, 没有才去竞争;非公平锁则直接竞争。
(1)非公平锁(公平)原理
加锁流程
有竞争
PART1:非公平模式(默认)
PART2:公平模式
(2)可重入原理
通过state++ 来实现
(3)可打断原理
不可打断模式(lock()方法)
这个模式下,即使线程被打断,线程仍然会留在AQS阻塞队列中,等获得锁后方能继续运行。线程只有获得锁后才能响应打断。
可打断模式(lockInterruptibly())
在这个模式下,可打断锁则会通过抛出异常的方式立刻响应打断,从而让线程停止在阻塞队列中等待。立即相应。
PART1:不可打断模式
PART2:可打断模式
(4)条件变量原理
传统对象等待集合只有一个 waitSet, Lock可以通过newCondition()方法 生成多个等待集合Condition对象。ReentrantLock的条件变量比 synchronized强大之处在于,它是支持多个条件变量的,这就好比synchronized 是那些不满足条件的线程都在一间休息室等消息。而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒。
简而言之:
await:创建conditionObject等待队列,线程加入到ConditionObject的链表中去,释放掉该线程所有的锁,把自己park住,然后唤醒下一个节点。
signal:必须要锁的持有者来调用该方法,把ConditionObject链表中的第一个线程转移到AQS的等待队列中。
3、读写锁
简而言之:
由于 state 是 int 类型的变量,在内存中占用4个字节,也就是32位。将其拆分为两部分:高16位和低16位,其中高16位用来表示读锁状态,低16位用来表示写锁状态。
当设置读锁成功时,就将高16位加1,释放读锁时,将高16位减1;
当设置写锁成功时,就将低16位加1,释放写锁时,将第16位减1
另外等待队列中节点会唤醒时,如果后续节点时读锁,那么它仍旧被唤醒,直至后面为写锁。
(1)图解
(2)原理(源码)
读锁(加锁,解锁)
写锁(加锁,解锁)
(3)应用(读写锁一致性缓存)
A: 图解
B: 代码
(4)Stampedlock
4、Semaphore
(1)应用
(2)原理
5、Countdownlatch
(1) 应用之同步等待多线程准备完毕
(2)应用之同步等待多个远程调用结束
6、Future
7、Cyclicbarrier
注意 CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为『人满发车』。