JavaAPI-concurrent-有锁API原理解析

目录

目的

JDK版本

Executor

interface Executor

interface ExecutorService

ThreadPoolExecutor

Executors

ScheduledThreadPoolExecutor

阻塞队列

copyOnWriteArrayList

copyOnWriteArraySet

CyclicBarrier

ConcurrentHashMap


目的

个人将concurrent包中的众多类分为加锁和不加锁两个分类,本文针对加锁分类进行介绍。

JDK版本

JDK8

Executor

interface Executor

价值

  • 提供[将 任务提交 与 任务执行机制(包括线程的使用与调度) 解耦]的方式
  • 避免显示的创建Thread。

JMM规定

  • ThreadA将任务提交到执行器  happens before  ThreadB执行任务

定义的接口

  • void execute(Runnable command)

interface ExecutorService

更具有扩展性的执行器接口,concurrent包中执行器都实现该接口。提供“管理终止” 和 以返回future的方式跟踪任务执行情况的方法。

JMM规定

  • ThreadA提交任务到ExecutorService之前的执行 happens before 任务被执行 happens before Future.get获取到结果

ThreadPoolExecutor

具有扩展性的线程池实现

解决两个问题

  • 提升表现,如性能/吞吐量;方式:减少每个任务的调用周期,即减少从提交到执行的时间。
  • 对执行任务时使用的资源进行边界划定与管理,如线程池中的线程

自定义线程池

  • corePoolSize and maxPoolSize
    通过构造函数进行设置,后续也可通过方法设置。setCorePoolSize/setMaximumPoolSize线程池会根据两者设定的范围,来自动调节poolsize.
  • 按需创建
    即默认情况下,只有当任务到达时,线程才会被构建与启动。当然可以通过重写进行自定义,以提前构建。
    prestartCoreThread/prestartAllCoreThreads
  • 创建线程
    使用ThreadFactory,需要保证线程安全。
  • 存活时间 Keep-alive times
    setKeepAliveTime
    allowCoreThreadTimeOut
  • 排队
    • 使用阻塞队列传输和持有提交的任务
    • 队列的用途和poolsize之间存在交互关系
      • 当poolsize小于core时,执行器直接使用任务新建线程,而不是排队。
      • 当poolsize大于等于core时,执行器将直接让任务去排队。
      • 当任务无法进入队列且poolsize小于max时,新建线程,否则抛弃任务。
    • 3中排队策略
      • 直接传送
        即任务直接传送给线程,不需要其他中间存储。
        问题:当任务到达速度>任务被消费的速度,线程无限增加
      • 无边界队列存储任务
        即所有任务都是先放到队列中存储,之后待消费。
        问题:
        poolsize始终维持在core,可能导致资源未合理使用,如cpu;
        增加了任务等待时间;
        当任务到达速度>任务消费速度时,任务队列无限赠长;
      • 有边界的任务队列
        队列和线程池边界的控制需要根据实际情况制定
        IO密集型 CPU密集型 任务量
    • 拒绝任务RejectedExecutionHandler
      • when
        • 执行器已经shutdown
        • 线程池和任务队列都已经饱和了
      • 默认4中策略
        • ThreadPoolExecutor.AbortPolicy 抛出异常RejectedExecutionException,以异常的形式提现现象
        • ThreadPoolExecutor.CallerRunsPolicy 提交任务的线程自行跑任务
        • ThreadPoolExecutor.DiscardPolicy 直接将新任务扔掉
        • ThreadPoolExecutor.DiscardOldestPolicy 直接扔掉最老的任务
    • 钩子方法
      • 用于操作执行环境
        beforeExecute/afterExecute/terminated
    • 任务队列查看
      使用getQueue获取任务队列,可用于仅仅限于监控/debug/获取任务数量等只读操作
      remove/purge当大量队列任务被取消时用于存储空间的回收
    • Finalization
      终止化
      为了避免线程池已不再使用后无法回收的问题,应该合理设置:
      keep-alive time/allowCoreThreadTimeOut

如何确定队列长度和线程数量

 明白线程池的内部原理后,我认为如下公式具有参考意义:

单位时间内产生的任务量 = 任务队列长度 + 单位时间内单个线程处理的任务量 * 最大线程量

可以保证不发生拒绝任务的情况 。

线程池poolsize与任务队列大小的关系图示

如何存储线程池状态和工人数量

线程池控制单元使用原子Integer类型,
高3 bit表示线程池状态;
低29bit表示线程池中有效工人的数量;(workerCount表示处于start和stop之间的线程数,包含正在退出但还没有被terminate的线程,所以workerCount >= lived thread的真实数量)

任务如何存储

private final BlockingQueue<Runnable> workQueue;

工人如何存储

private final HashSet<Worker> workers = new HashSet<Worker>();

工人模型如何实现

  • Worker是AQS和runnable的实现体,不允许重入,使用AQS的state表示工人正在工作。
    • private final class Worker
              extends AbstractQueuedSynchronizer
              implements Runnable

  • 属性
    •         /** Thread this worker is running in.  Null if factory fails. */
              final Thread thread;工人在这个线程中执行任务
              /** Initial task to run.  Possibly null. */
              Runnable firstTask;需要执行的任务
              /** Per-thread task counter */
              volatile long completedTasks;工人已经完成的任务计数器

  • 工人时如何工作的
    • 将自己作为参数,回调线程池的runWorker方法。
      •         public void run() {
                    runWorker(this);
                }

    • 主要流程
      • 循环从任务队列中获取任务,并使用idle作为超时时间;
      • 如果任务存在,则依次执行beforeExecute task.run afterExecute,如果执行期间出现异常,则退出循环且进入线程退出线程池的操作;
      • 如果任务不存在,则退出循环且进入线程退出线程池的操作;
      • 强调2点:worker执行过程出现异常;线程池空闲,长时间无新任务。
      • 退出线程池主要流程:
        • 从wokerSet中移除当前worker;
        • 如果时异常退出 或者 当前工人数少与下限workerCount<corePoolSize,则新增普通worker;
  • 可能引起误区的方法
    • getTaskCount获取已经被执行过task的数量+任务队列大小
    • getLargestPoolSize获取历史线程池数量最高点

Executors

提供了很多方便的执行器工厂方法

缺点

  • 构造线程池时无法进行自定义,例如idle,maxPoolSize,workerQueue,拒绝策略;
  • 使用的workerQueue类型时无限的new LinkedBlockingQueue<Runnable>(),这可能会引起内存紧张。
  • idle基本设置为0,这样导致idle毫无意义,可能频繁新建和销毁线程。

所以尽可能自行创建线程池,让一切都在控制范围内,不使用这些工厂方法。

ScheduledThreadPoolExecutor

 extends ThreadPoolExecutor implements ScheduledExecutorService

如何实现按照时间进行调度的

  • 任务队列模型
    • DelayedWorkQueue  extends AbstractQueue<Runnable>
              implements BlockingQueue<Runnable>

      在进行take/poll的逻辑中会判断task是否该执行

  • 任务模型
    • ScheduledFutureTask<V>
                  extends FutureTask<V> implements RunnableScheduledFuture<V>

  • 主要field
    • private long time;下次执行的绝对时间nanoTime,从任务队列取时,会通过该数据判断是否该执行。
    • private final long period;整数表示fix-rate固定频率执行,负数表示fix-delay固定延期执行,0表示只延迟执行一次。
  • run任务执行逻辑 
    • 如果线程池当前已不允许执行任务,则cancel
    • 如果是非周期性任务,则执行
    • 如果是周期任务,则执行,计算下次执行时间,再次放入任务队列

阻塞队列

使用lock和condition,await signal实现

copyOnWriteArrayList

  • 采用volatile+lock
  • 线程安全的ArrayList;虽然消耗高,但在[遍历(读)操作场景远超于写操作]且不可能也不应该使用synchronized时,其更高效。
  • 与ArrayList一致,使用[数组]作为数据存储, volatile Object[] array;
  • 写加锁,读不加锁
  • 提供了线程安全的修改操作,锁+副本操作
    • 使用重入锁lock
    • 复制当前数组得到其副本,Arrays.copyOf
    • 对副本进行操作
    • 使用副本覆盖原数组
    • 最后释放重入锁unlock
  • 每次写操作后,array引用新数组,原数组被抛弃。
  • 提供的Iterator,数据源为遍历器创建时的array所指向的实际数组,写操作不影响遍历,因为写后array引用已发生变化。该遍历器不支持修改操作。

copyOnWriteArraySet

  • 基于copyonwritearraylist实现的set,使用场景读远大于写
  • final CopyOnWriteArrayList<E> al,set的唯一性使用al.addIfAbsent实现

多个线程间流程同步协调器-CyclicBarrier

  • 一组线程中,每个线程一直等待,直到组内其他n-1个线程也到达了障碍物点。count可重置。
  • 涉及
    ReentrantLock
    Condition 在这里充当障碍物,signalAll代表障碍物倾倒。
    condition.await/signalAll
  • await([timeout])大致流程
    • 加锁
    • count减1
    • 若count为0则执行commond,condition.signalAll,count恢复等,return;
    • 否则loop  condition.await 直到障碍物倾倒/超时/线程中断等(当与lock关联的condition await时,其lock会自动释放)

ConcurrentHashMap

  • 元素存储单元
    • Node impl Map.Entry
      hash//key.hash进行XORs((h ^ (h >>> 16)) & HASH_BITS)后的数据,目的:高16位移到低16位然后高16位设0
      key
      volatile value//可见性
      volatile next//可见性

  • 元素存储
    • volatile Node[] table
      volatile Node[] nextTable//transfer时使用,即数组扩大或减小

  • 操作Note[]时均使用Unsafe,依赖jvm实现
    • tabAt获取Node[index],使用到getObjectVolatile
    • tabAt获取Node[index],使用到getObjectVolatile
    • setTabAt设置Node[index],使用到putObjectVolatile
  • 需对具体Node[index]进行写操作时,使用synchronized锁,粒度为Node[index]
    • Node f = tabAt(tab, i = (n - 1) & hash);
      synchronized(f){
      //xxx
      }

    • 即使用Note[index]链表的第一个Note的监视器锁

    • 所以扩容后,锁数量会增加。

  • 读操作没有锁!
  • 计数原理
    • 不保证准确,因为无锁,例如遍历到index=10时,cell[4]数据发生变化。
    • volatile cell[] 计数cell数据存储每个线程put的数据数量,遍历叠加cell[]获取当前总数据量。
    • put()时,会获取当前线程对应的cell:cell[unsafe获取当前线程的探针&cell.length],然后unsafe cas设置新数(+1)。
  • 每次put之后,均会判断是否需要transfer交换。交换过程采用CAS+同步tab[i]结合的方式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值