public RecordAppendResult append(TopicPartition tp,
long timestamp,
byte[] key,
byte[] value,
Callback callback,
long maxTimeToBlock) throws InterruptedException {
// We keep track of the number of appending thread to make sure we do not miss batches in
// abortIncompleteBatches().
appendsInProgress.incrementAndGet();
try {
// check if we have an in-progress batch
Deque<RecordBatch> dq = getOrCreateDeque(tp);
// 做并发开发时,无时无刻都要想着总会有至少两个线程争抢资源(悲观)
// dq内部没有设计成线程安全,所以并发使用dq时要加锁
synchronized (dq) {
if (closed)
throw new IllegalStateException("Cannot send after the producer is closed.");
RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
if (appendResult != null)
return appendResult;
}
// 同步结束,下面代码有并发 (注1)
// 若这次append失败(内存不够),则执行以下代码(再使用ByteBuffer分配一次内存)
// we don't have an in-progress record batch try to allocate a new batch
int size = Math.max(this.batchSize, Records.LOG_OVERHEAD + Record.recordSize(key, value));
log.trace("Allocating a new {} byte message buffer for topic {} partition {}", size, tp.topic(), tp.partition());
// 对应(注1),此时可能至少两个线程append失败,同时为各自线程都分配了一个新的buffer
// 以下分析针对线程1 和 线程2
ByteBuffer buffer = free.allocate(size, maxTimeToBlock);
// step1: 同步开始,即使两个线程都各自分配了一次内存,不过同一时间只允许一个线程操作dq
synchronized (dq) {
// Need to check if producer is closed again after grabbing the dequeue lock.
if (closed)
throw new IllegalStateException("Cannot send after the producer is closed.");
// step2:假设第一个线程先抢到dq,执行tryAppend
// step6:假设第一个线程执行完毕,之后线程2抢到了dq
RecordAppendResult appendResult = tryAppend(timestamp, key, value, callback, dq);
if (appendResult != null) {
// step7:线程2 tryAppend 成功(使用了线程1的内存),释放之前分配给线程2的内存
// Somebody else found us a batch, return the one we waited for! Hopefully this doesn't happen often...
free.deallocate(buffer);
return appendResult;
}
// step3:tryAppend失败(虽然分配了内存,不过还没有真正用到这个内存),执行下面的代码
MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());
// step4:tryAppend成功
FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, key, value, callback, time.milliseconds()));
// step5:将分配给线程1的内存加入到dq进行管理,以供其它线程使用
dq.addLast(batch);
incomplete.add(batch);
return new RecordAppendResult(future, dq.size() > 1 || batch.records.isFull(), true);
}
} finally {
appendsInProgress.decrementAndGet();
}
}
Q:为什么分了两个局部synchronized,而不使用一个完整的synchronized?
A:减少锁的持有时间,增加吞吐量。
假设线程1所需的内存特别大,需要花大量时间分配内存,而线程2需要的内存很少,可以利用其它线程的内存。如果是一个完整的synchronized,且线程1先抢到dq锁,则线程2也要跟着线程1一起等待。
如果是分了局部的synchronized,则线程1抢到dq锁后,不必直到分配了足够内存给线程1后才释放dq,可以在适当位置提前释放dq,让线程2有机会先执行。
分局部的synchronized的缺点就是线程执行逻辑复杂了,比如上述kafka源码的step7。