• 一个多线程Reactor模型的Bug:线程安全一定要把构造方法考虑在内


     

      众所周知,JVM 创建一个对象分三步:

      1.在堆内存开辟内存空间。

      2.在堆内存中实例化Car里面的各个参数。

      3.把对象指向堆内存空间。

      为了提高运行效率,编译器在编译代码时可能会对指令进行重排序。重排序的原则是,保证单线程执行结果的正确性,并遵循 happen-before 原则。

      指令间的依赖关系包括数据依赖和控制依赖。对于控制依赖,处理器 本身存在流水线冒险等行为,处理器层面会对存在控制依赖的指令进行结果检查,该类指令优化不在编译器层面。而对于存在数据依赖的指令,如果打乱顺序,显然会造成单线程情况下执行结果的变化。所以编译器不会打乱存在数据依赖的指令的顺序。

      上述三步中的 2、3 两步并不存在数据依赖关系,单线程情况下将其顺序颠倒并不会导致执行结果的变化,并且这两步不在 happen-before 原则范围内,因此在编译过程中可能会被打乱顺序。

      但在多线程环境下,这两步是可能存在数据依赖关系的。

      比如一个比较复杂的例子,NIO 编程时,我们为 channel 注册了一个感兴趣的事件,并绑定一个 handler 实例:

       如果事件触发,将会在一个新线程执行 handler 中的 run 方法,handler 的构造方法如下:

       构造方法会初始化成员变量。run 方法中会使用这些变量:

       如果在第一张图中,new Handler 时,2、3 步 被乱序执行,那么第三张图 read 函数调用会报空指针异常。因为指向 handler 对象存储空间的指针首先被返回赋值给了 channel 的 attachment ,但 handler 对象的成员还没有被初始化,所以 成员变量 socketChannel 还是空的。

      事实也证实了这一点:

      我们要保证的是,执行 handler 的 run 之前,handler 的构造方法得到完全执行,并且多核环境下执行结果对其它线程(核心)可见。

      因此多线程环境下,想要程序按预想的情况执行,我们需要保证两点:

      1. 构造函数的执行早于 run 函数的执行。

      2. 构造函数必须完全执行,且结果对所有线程可见,才可以执行 run 方法。

      对于 1, 我们在执行 run 之前可以对 attachment 进行判空,空的话什么都不做,等待事件下次触发再判断构造函数是否已经执行(感谢 NIO 的水平触发)。

      对于 2,我们在 1 的基础上,通过锁使得 构造函数 与 run 方法互斥即可实现。通过 synchronized 的锁和其 monitorEnter 后、monitorExit 前的内存屏障可以充分保证可见性和有序性。一旦构造函数先执行,run 一定等待其执行完成才会执行;若构造函数未执行,run 立即返回等待下次被触发。

      构造函数加锁:

      run 函数加锁:

        在写多线程代码时,如果存在类成员变量等线程共享变量,一定要注意其线程安全性。在写上述代码时(一个 Reactor 模型),因为一个 handler 对象只会被一个线程调用,因此忽略了对线程安全的考虑。但最后出错才发现,还有另一个线程在调用构造方法!这样便是两个线程操作 handler 对象,一个调用构造函数、一个调用其它函数。

      考虑线程安全问题,构造方法一定要考虑在内!

      作如上修改后,问题解决,大剂量测试没有异常发生:

  • 相关阅读:
    [HDOJ4788]Hard Disk Drive(水题)
    [HDOJ4782]Beautiful Soup(模拟)
    [HDOJ3652]B-Number(数位dp)
    [CF55D]Beautiful numbers(数位dp,状态压缩)
    [HDOJ3555]Bomb(数位DP)
    [HDOJ2089]不要62(数位DP)
    [HDOJ5881] Tea(找规律)
    [HDOJ5900]QSC and Master(区间dp)
    [HDOJ5878]I Count Two Three(暴力枚举,二分)
    [HDOJ5879]Cure(求极限,打表)
  • 原文地址:https://www.cnblogs.com/niuyourou/p/13047177.html
Copyright © 2020-2023  润新知