Java多线程(五)

109 阅读9分钟

七、 共享模型之工具(主要是线程池)

1、自定义线程池

图片1.png

概念

阻塞队列中维护了由主线程(或者其他线程)所产生的的任务

主线程类似于生产者,产生任务并放入阻塞队列中

线程池类似于消费者,得到阻塞队列中已有的任务并执行

实现了一个简单的线程池的思路

1. 阻塞队列BlockingQueue用于暂存来不及被线程执行的任务

  • 也可以说是平衡生产者和消费者执行速度上的差异
  • 里面的获取任务和放入任务用到了生产者消费者模式
  1. 线程池中对线程Thread进行了再次的封装,封装为了Worker
  • 在调用任务的run方法时,线程会去执行该任务,执行完毕后还会到阻塞队列中获取新任务来执行
  1. 线程池中执行任务的主要方法为execute方法
  • 执行时要判断正在执行的线程数是否大于了线程池容量

或者

步骤1:自定义拒绝策略接口

步骤2:自定义任务队列

阻塞队列类

成员变量:任务队列,锁,生产者条件,消费者条件,容量

成员方法:阻塞添加,阻塞获取,带超时阻塞添加,带超时阻塞获取,数量,带策略阻塞添加

步骤3:自定义线程池

线程池类

构造方法:

成员变量:任务队列,线程集合,核心线程数,获取任务的超时时间,拒绝策略

成员方法:执行任务

线程类(worker)继承

成员变量: Runnable

成员方法:run

步骤4:测试

自定义线程池类

多个线程执行

2、ThreadPoolExecutor

图片2.png

(1)线程池的状态 图片3.png

(2)构造方法

图片4.png

图片5.png

工作方式:当一个任务传给线程池以后,可能有以下几种可能

  • 将任务分配给一个核心线程来执行
  • 核心线程都在执行任务,将任务放到阻塞队列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)提交任务

图片6.png

图片7.png

(7)关闭线程池

图片8.png

图片9.png

图片10.png  

3、设计模式 (异步模式之工作线程)

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

工作线程本质上是分工。不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率。例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工。

4、Timer 相关 (quarts)

Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。

5、ScheduledThreadPoolExecutor

scheduleAtFixedRate 例子(任务执行时间超过了间隔时间),由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s

scheduleWithFixedDelay 例子。这个不会产生间隔效果。

6、处理异常

方法1:主动捉异常

方法2:使用 Future

7、Tomcat线程池

图片11.png

图片12.png

图片13.png

8、Fork/Join

图片14.png

 八、锁相关 ( AQS 和ReentrantLock)

1、AQS概述

(1)定义

图片15.png

(2)实现自定义锁

自定义同步器extends AbstractQueuedSynchronizer

(tryAcqurie, tryRelease, condition)

自定义锁,有了自定义同步器,很容易复用 AQS ,实现一个功能完备的自定义锁

(3)理解

AQS 要实现的功能目标

阻塞机制,阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire

获取锁超时机制

通过打断取消机制

独占机制及共享机制

条件不满足时的等待机制

图片16.png

2、Reentrantlock

简单说

加锁过程

成功,结束。

失败,进入队列。

      尝试,2次后,没有获得(有公平和非公平),挂起(线程状态为-1)。

线程释放锁的过程

      如果没有其他线程抢,等待队列中可以获得。

      如果有其他线程抢,并且成功。

公平和非公平的区别:

  1. 公平锁

想要获取锁资源的线程在队列里进行排队等待,新来的线程去队尾排队,锁资源释放的时候只有队首线程可以获得锁资源。

  1. 非公平锁

新来的线程直接和队首线程争抢锁资源,如果争抢到了,则直接获取锁资源,队首线程继续等待。 如果新来的线程竞争失败,则去队尾进行排队,只能等待队列前所有线程执行完毕后自己才能获取锁。

注意,公平锁与非公平锁主要区别在于 tryAcquire 方法的实现;公平锁会先检查 AQS 队列中是否有前驱节点, 没有才去竞争;非公平锁则直接竞争。

(1)非公平锁(公平)原理

加锁流程

图片17.png

有竞争

图片18.png

图片19.png

图片20.png

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的等待队列中。

图片21.png

图片22.png

图片23.png

3、读写锁

简而言之:

由于 state 是 int 类型的变量,在内存中占用4个字节,也就是32位。将其拆分为两部分:高16位和低16位,其中高16位用来表示读锁状态,低16位用来表示写锁状态。

当设置读锁成功时,就将高16位加1,释放读锁时,将高16位减1;

当设置写锁成功时,就将低16位加1,释放写锁时,将第16位减1

另外等待队列中节点会唤醒时,如果后续节点时读锁,那么它仍旧被唤醒,直至后面为写锁。

(1)图解

图片24.png

图片25.png

图片26.png

图片27.png

图片28.png

图片29.png

图片30.png

图片31.png

图片32.png

图片33.png

(2)原理(源码)

读锁(加锁,解锁)

写锁(加锁,解锁)

(3)应用(读写锁一致性缓存)

A: 图解

图片34.png

图片35.png

图片36.png

B:  代码

(4)Stampedlock

图片37.png

4、Semaphore

(1)应用

图片38.png

(2)原理

图片39.png

图片40.png

图片41.png

5、Countdownlatch

(1) 应用之同步等待多线程准备完毕

(2)应用之同步等待多个远程调用结束

6、Future

7、Cyclicbarrier

注意 CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为『人满发车』。

图片42.png