读kafka源码的笔记 (1) - 减少锁的持有时间

通过将synchronized分为局部锁来降低锁的持有时间,以提高并发吞吐量。这种方式允许内存需求小的线程在大内存需求线程分配内存时有机会执行,避免了不必要的等待,但可能导致代码执行逻辑更复杂。
摘要由CSDN通过智能技术生成
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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值