Java并发-Alluxio Metrics实践

介绍

Alluxio中有很多并发性的代码,这些并发性的代码需要经过严整的测试,其中这些测试主要依赖于ScheduledTimer.java、HeartbeatScheduler.java、HeartbeatThread.java。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-COqiOc2Q-1600501389124)(quiver-image-url/99C497BEBB705BBBC3B472B0636EBE7F.jpg =985x555)]

  1. 上图中SleepingTimer类主要是用于Alluxio Metrics指标上报,用于控制指标周期性进行统计上报;
  2. ScheduledTimer类主要用于验证系统在高并发场景下的正确性;

Alluxio并发测试调试

下面主要是从ScheduledTimer类出发探讨Java并发在Alluxio中的实践:

主要流程如下图所示,新建一个线程HeartbeatThread thread1(每个HeartbeatThread绑定一个ScheduledTimer),线程thread1会执行timer.tick(),此时tick会先调用HeartbeatScheduler.addTimer(),然后await()阻塞,在thread1父线程会循环执行HeartbeatScheduler.execute(threadName),每次execute,HeartbeatSchduler会判断当前线程是否拥有可重入锁(HeartbeatScheduler.sLock),有则进入,上面提到的调用HeartbeatScheduler.addTimer()会将当前线程thread1绑定的timer加入map中,并signalAll,此时所有线程进入就绪状态,而刚刚拿到可重入锁的线程会判断其timer是否加入到map,如果没有则继续await,下一个线程重复同样的步骤,直到map中记录了该线程的timer(即线程执行了timer.tick()),下面HeartbeatScheduler会调用timer.schedule(),之后timer绑定的线程被唤醒,跳出循环,tick()结束,thread执行executor.heartbeat()计数加一。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iCZwR9I8-1600501389127)(quiver-image-url/E711DEACD6752DAD1C151D81EF6AE6F5.jpg =382x181)]

多线程并发行为解析:

HeartbeatScheduler -> 所有线程共享一个HeartbeatScheduler静态方法类(类锁)

当前持有r锁的线程 2进入等待队列condition
reentranL: 4 3 7 5 9 10
condition: null

await()=================

当前持有r锁的线程 1进入等待队列condition
reentranL: 4 3 7 5 9 10 6 8
condition: 2 

await()=================

当前持有r锁的线程 4进入等待队列condition
reentranL:   3 7 5 9 10 6 8
condition: 2 1 

await()=================

当前持有r锁的线程 3进入等待队列condition
reentranL:     7 5 9 10 6 8
condition: 2 1 4 

await()=================

当前持有r锁的线程 7进入等待队列condition
reentranL:       5 9 10 6 8
condition: 2 1 4 3  

await()=================

当前持有r锁的线程 5进入等待队列condition
reentranL:         9 10 6 8
condition: 2 1 4 3 7 

await()=================

当前持有r锁的线程 9进入等待队列condition
reentranL:           10 6 8
condition: 2 1 4 3 7 5

await()=================

当前持有r锁的线程 10进入等待队列condition
reentranL:              6 8
condition: 2 1 4 3 7 5 9

await()=================

当前持有r锁的线程 6进入等待队列condition
reentranL:                8
condition: 2 1 4 3 7 5 9 10

await()=================

当前持有r锁的线程 8进入等待队列condition
reentranL:                null
condition: 2 1 4 3 7 5 9 10 6

********************************触发signalAll()
当前持有r锁的线程 2 -> 2跳出await(),并执行schedule()
reentranL: 1 4 3 7 5 9 10 6 8 h5
condition: null

当前持有r锁的线程 1
reentranL:   4 3 7 5 9 10 6 8 h5
condition: null

当前持有r锁的线程 4
reentranL:     3 7 5 9 10 6 8 h5 2(2又重新回到同步队列:上面提到2要执行schedule(),所以需要再次获取lock)
condition: null

当前持有r锁的线程 1(1又获取到锁,是因为它在没有进入同步队列就开始争夺锁,从而进入schedule())
reentranL:     3 7 5 9 10 6 8 h5 2 4
condition: null
### 接上面一步,1进而调用ScheduledTimer.schedule(),此时ScheduledTimer对象中:
### curThread = 1
### reentranL: 1
### condition: h0
### 此时,1执行mTickCondition.signal()唤醒h0,并从HeartbeatScheduler.sTimers中移除h0绑定的当前timer对象
### 1执行完ScheduledTimer.schedule()后再次执行await等待下一次h0 tick()时被唤醒(tick() -> addTimer() -> condition.signalAll())
### 由于上面1唤醒h0线程,所以h0发现mScheduled为true,则跳出循环,h0线程本次tick()结束

当前持有r锁的线程 3
reentranL:   7 5 9 10 6 8 h5 2 4
condition: null

ScheduledTimer -> 每个线程一个ScheduledTimer对象(对象锁)

curThread = h8
reentranL: null
condition: h8

curThread = h6
reentranL: null
condition: h6

curThread = h1
reentranL: null
condition: h1

curThread = h3
reentranL: null
condition: h3

curThread = h4
reentranL: null
condition: h4

curThread = h7
reentranL: null
condition: h7

curThread = h0
reentranL: null
condition: h0

curThread = h9
reentranL: null
condition: h9

curThread = h2
reentranL: null
condition: h2

并发测试分析

上面介绍到Alluxio的并发测试主要涉及三个类,ScheduledTimer.java、HeartbeatScheduler.java和HeartbeatThread.java
这里借助上面的调试,可以总结一下三个类的作用:

  • HeartbeatThread
    此类不是线程安全的,其作用就是创建一个绑定了HeartbeatTimer(测试都采用ScheduledTimer)对象和HeartbeatExecutor(HeartbeatThreadTest采用DummyHeartbeatExecutor)对象的线程。其中DummyHeartbeatExecutor对象主要用于任务的执行,即每次heartbeat执行counter计数加一

  • ScheduledTimer
    此类主要是延迟(定时)调度HeartbeatThread类型的线程中的任务(HeartbeatExecutor),正如上面的调试提到的tick(),HeartbeatThread中会在执行mExecutor任务之前通过其绑定的ScheduledTimer来执行tick(),tick()在没有HeartbeatScheduler的帮助下会阻塞住,进入condition等待队列。

  • HeartbeatScheduler
    正如名字所示,此类主要是起到心跳调度的作用,调度的是HeartbeatThread线程绑定的ScheduledTimer,上面介绍到ScheduledTimer.tick()在没有HeartbeatScheduler帮助下会阻塞进入condition等待队列,此时主线程会执行HeartbeatScheduler.execute() (依次执行await()、schedule()、await())来唤醒阻塞在ScheduledTimer.condition等待队列中的HeartbeatThread线程,此时HeartbeatThread线程走出ScheduledTimer.tick(),并执行mExecutor.heartbeat()做计数加一动作。

  • 涉及到哪些Java并发知识点

  1. AQS: 同步和等待队列
  2. 可重入锁
  3. 对象安全、可见性
  4. 线程中断、取消
  5. 线程池

并发知识点总结归纳

对于上面涉及到的Java并发知识点,逐个翻阅书籍与博客,深入理解其实现原理、运行机制及应用场景实践

可重入锁与AQS

  • 公平锁/非公平锁

简单来说:
非公平锁: 就是后来要获取锁的线程不会老老实实的进入同步队列(FIFO)通过排队获取锁,而是直接尝试获取锁,获取不到再进入同步队列;
公平锁: 与非公平锁是相对的,每个要获取锁的线程都老老实实的进入同步队列(FIFO)通过排队来获取锁。

从源码从面上,公平锁和非公平锁有两处不同:

  1. 非公平锁在调用lock后,首先就会调用CAS进行一次抢锁,若此时恰巧锁没有被占用,则直接就获取到锁返回
static final class FairSync extends Sync {
   final void lock() {
       acquire(1);
   }
  // ...
}

static final class NonfairSync extends Sync {
   final void lock() {
       // 1. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
       if (compareAndSetState(0, 1))
           setExclusiveOwnerThread(Thread.currentThread());
       else
           acquire(1);
   }
   // ...
 }
  1. 非公平锁在CAS失败后,和公平锁一样都会进入到tryAcquire方法,在tryAcquire方法中,如果发现锁此时被释放了(state==0,则非公平锁会直接CAS抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,老老实实排在队尾
// FairSync
protected final boolean tryAcquire(int acquires) {
       final Thread current = Thread.currentThread();
       int c = getState();
       if (c == 0) {
           // 2. 和非公平锁相比,这里多了一个判断:是否有线程在等待
           if (!hasQueuedPredecessors() &&
               compareAndSetState(0, acquires)) {
               setExclusiveOwnerThread(current);
               return true;
           }
       }
       // ...
}
//NonfairSync
final boolean nonfairTryAcquire(int acquires) {
   final Thread current = Thread.currentThread();
   int c = getState();
   if (c == 0) {
       // 这里没有对阻塞队列进行判断
       if (compareAndSetState(0, acquires)) {
           setExclusiveOwnerThread(current);
           return true;
       }
   }
   // ...
}
  • 可重入锁
    ReentrantLock在构造时默认是非公平的,但可以通过参数来控制。
// ReentrantLock的构造函数
public ReentrantLock() {
        sync = new NonfairSync();
    }

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
  • AQS(AbstractQueuedSynchronizer)

同步队列、条件队列中Node节点数据结构:

volatile int waitStatus; // 可取值 0、CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;

同步(阻塞)队列和条件队列区别是什么:

  1. 同步(阻塞)队列是双向链表,条件队列是单向链表;
  2. 条件队列中的节点需要转移到到同步(阻塞)队列等待获取锁;
  3. 对于同一个锁(比如可重入锁)可以有多个条件队列,但只能有一个同步(阻塞)队列

同步队列:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Iwxd45Rk-1600501389129)(quiver-image-url/76A1F5AC8DF69651BFE6B9142E54D4A0.jpg =947x352)]

同步队列大致流程如上图所示,其中更加细节的点,比如:同一时间有第二个线程想要获取锁的时候,AQS的同步阻塞队列才会得到初始化(通过自旋来设置head节点);以及中间会有线程取消阻塞等待,则其waitstatus会被置为取消(1),后面当阻塞队列中需要有线程被唤醒的时候,这些被取消的线程节点会被忽略;以及线程在抢夺锁失败之后会尝试加入同步阻塞队列尾部,但可能会失败(同一时间会有其他线程尝试加入同步阻塞队列),这时候线程也会通过自旋的方式进入同步阻塞队列等等,大家可以参照源码来细看。

条件队列:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XUQ6qzWv-1600501389131)(quiver-image-url/BA9ADFADF6720062985F91A0C853898A.jpg =437x256)]

同步队列于阻塞队列的关系大致如上图所示,其中一些细节还需要再看几遍源码才能完全掌握,这里就不擅自总结了。

  • 一个 ReentrantLock 实例可以通过多次调用 newCondition() 来产生多个 Condition 实例,这里对应 condition1 和 condition2。注意,ConditionObject 只有两个属性 firstWaiter 和 lastWaiter;
  • 每个 condition 有一个关联的条件队列,如线程 1 调用 condition1.await() 方法即可将当前线程 1 包装成 Node 后加入到条件队列中,然后阻塞在这里,不继续往下执行,条件队列是一个单向链表;
  • 调用condition1.signal()触发一次唤醒,此时唤醒的是队头,会将condition1对应的条件队列的firstWaiter(队头)移到阻塞队列的队尾,等待获取锁,获取锁后await方法才能返回,继续往下执行。
  • 线程中断

线程中断:中断不是类似linux里面的kill -9 pid命令,不是说中断某个线程,线程就停止运行了。中断代表线程状态,每个线程都关联了一个状态,是一个true或false的boolean值,初始值为false。

如果线程处于以下三种情况,那么当线程被中断的时候,能自动感知到:

  • 来自Object类的wait()、wait(long)、wait(long, int),
    来自Thread类的join()、join(long)、join(long, int)、sleep(long)、sleep(long, int),这几个方法的相同之处是方法上都有: throws InterruptedException,如果线程阻塞在这些方法上,此时如果其他线程对这个线程进行了中断,则这个线程会从这些方法中立即返回,抛出InterruptedException异常,同时重置中断状态为false;
  • 实现了InterruptedChannel接口的类中的一些I/O阻塞操作,如DatagramChannel中的connect方法和receive方法等(大多数标准等Channel都实现了InterruptibleChannel);
  • Selector的异步I/O,若一个线程在调用Selector.select方法时阻塞了,则调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。

附录

  • 参考文章

1. AbstractQueuedSynchronizer同步队列与Condition等待队列
2. 基于ReentrantLock理解AQS设计原理 ❌
3. 一行一行源码分析清楚 AbstractQueuedSynchronizer<一>
4. 一行一行源码分析清楚 AbstractQueuedSynchronizer<二>
5. 一行一行源码分析清楚 AbstractQueuedSynchronizer<三>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值