减小锁的粒度
总说周知,使用synchronized 修饰的代码块在单位时间内只允许一个线程操作临界资源,这使得其他无法操作临界资源的线程都会阻塞在synchronized所修饰的临界资源的所持有的ObjectMonitor的_WaitSet 中:
所以通过减小锁的粒度,可以保证高并发场景下等待的线程可以尽可能快的得到临界资源,从而提升程序整体执行的并发度:
这一点Netty中内存池的源码PoolArena的计算活跃分配数量的方法numActiveAllocations的处理就做的非常出色,可以看到它在计算val时并没有锁住整个方法,而是在需要进行临界计算的部分加一个synchronized 关键字:
@Override
public long numActiveAllocations() {
//运算
long val = allocationsSmall.value() + allocationsHuge.value()
- deallocationsHuge.value();
//必要的部分上锁
synchronized (this) {
val += allocationsNormal - (deallocationsSmall + deallocationsNormal);
}
return max(val, 0);
}
减小空间占用
netty通过totalPendingSize计算待发送的数据大小,单位为long,为了保证并发计算的准确性,netty将其设置为volatile保证可见性,再通过AtomicLongFieldUpdater将其封装为原子类:
private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER =
AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");
@SuppressWarnings("UnusedDeclaration")
private volatile long totalPendingSize;
那么问题来了?为什么不直接使用AtomicLong类型呢? 总说周知,包装类内部包含markword等对象头字段,整体大小远大于基类型,所以netty为了保证CPU缓存能够一次性加载totalPendingSize就将其设置为基类型,并自封装为原子类进行操作,在保证线程安全的同时,又能保证CPU缓存加载和执行的效率:
提高锁的效率
对于需要进行计数但不经常查看结果的变量,Netty使用LongAdder 而不是AtomicLong:
@SuppressJava6Requirement(reason = "Usage guarded by java version check")
final class LongAdderCounter extends LongAdder implements LongCounter {
@Override
public long value() {
return longValue();
}
}
这里笔者也简单介绍一下原因,前者进行并发的时候会让线程计算随机运算得到LongAdder内部计数数组的某个位置,LongAdder会根据当前计数线程竞争情况对数组进行动态扩容,最终在计算结果的时候,需要将数组中的所有元素结合起来。 所以这种数据结构对于并发累加操作性能表现比较好,但是对于统计就表现的差一点,这一点对于LongAdderCounter 这种不定时统计来说再合适不过:
选用合适的并发技巧
NioEventLoop采用无锁串行化的设计思路,通过整体并发,局部串行的方式替代多线程消费单个阻塞队列的方案,这种方案结合了netty中多连接少量线程处理的场景,采用mpsc这种多消费者单生产者队列让并发的线程提交移步任务交给少量的nio线程处理,在整体并行的同时,通过优化eventLoop线程逻辑又能让eventLoop线程能够高效的串行处理:
private void execute(Runnable task, boolean immediate) {
boolean inEventLoop = inEventLoop();
//提交任务到mpsc队列中
addTask(task);
//如果当前提交任务的不是eventLoop线程,则从eventLoopGroup中启动一个线程
//......
if (!addTaskWakesUp && immediate) {
wakeup(inEventLoop);
}
}
然后NIO线程每次循环时调用runAllTasks有序执行:
protected boolean runAllTasks(long timeoutNanos) {
//......
for (;;) {
//执行任务
safeExecute(task);
runTasks ++;
// Check timeout every 64 tasks because nanoTime() is relatively expensive.
// XXX: Hard-coded value - will make it configurable if it is really a problem.
if ((runTasks & 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}
//从mpsc中有序获取
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}
小结
本文基于源码的角度分析了netty中的并发技巧,希望对你有帮助。