基于 Netty 源码学习那些并发技巧

开发
Netty中许多非常巧妙地并发开发技巧,所以本文将深入源码分析介绍Netty中的那些技巧,希望对你有帮助。

减小锁的粒度

总说周知,使用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中的并发技巧,希望对你有帮助。

责任编辑:赵宁宁 来源: 写代码的SharkChili
相关推荐

2024-11-22 00:09:15

2021-07-10 08:04:07

Reactor模式Netty

2022-03-04 08:10:35

NettyIO模型Reactor

2024-12-04 13:52:30

2022-09-09 08:41:43

Netty服务端驱动

2021-05-24 10:55:05

Netty单机并发

2022-11-27 08:12:11

RocketMQ源码工具类

2010-04-26 09:26:09

JavaScript

2018-05-31 13:50:30

Java高并发

2022-04-16 16:52:24

Netty网络服务器客户端程序

2022-03-03 08:01:41

阻塞与非阻塞同步与异步Netty

2016-12-28 13:19:08

Android开发坑和小技巧

2021-11-24 08:55:38

代理网关Netty

2010-02-23 16:46:47

WCF并发能力

2022-10-17 08:07:13

Go 语言并发编程

2021-09-27 08:56:44

NettyChannelHand架构

2024-12-03 15:45:39

Python元组编程

2019-12-31 10:33:57

Netty高性能内存

2018-04-20 09:36:23

NettyWebSocket京东

2024-11-05 18:18:48

点赞
收藏

51CTO技术栈公众号