Scala静态全局值先声明后赋值的方法及NPE问题排查
Scala伴生对象静态变量在Spark集群中的NPE问题
核心目标
希望在类中使用静态val,避免将其作为函数参数传递。
实现方式
因需要静态特性,将其声明在伴生对象中,但因某些原因无法在声明时赋值,采用如下方案:
case class DemoParams(name: String) class Demo { def foo = { println("Demo params name is: ", Demo.demoParams.name) // Works fine anotherFoo(Demo.demoParams.name) // Throws NPE ! } def anotherFoo(someName: String) = { // some code } } object Demo { var demoParams: DemoParams = _ // Declare here def apply() = new Demo() def run = { demoParams = DemoParams(name = "Salmon") // Define here val demoObj = Demo() demoObj.foo } def main() = { run } } Demo.main()
运行现象
在Spark集群运行时,能够打印Demo.demoParams,但将Demo.demoParams.name传递给另一个函数时会抛出NullPointerException。
问题
- 这是否是先声明静态值后赋值的正确方式?希望使用不可变val而非var,是否有更优方案?
- 为何传递
Demo.demoParams.name给函数时会出现NPE?
解答
问题1:静态值声明赋值方式与val替代方案
你当前使用var demoParams: DemoParams = _的方式并非最佳实践:var的可变性会引入线程安全问题,且初始化为null的设计本身就容易触发空指针,在Spark分布式环境下风险更高。
如果要使用不可变的val,推荐以下几种方案:
- 广播变量方案:将
DemoParams通过Spark广播变量分发到所有Executor节点,在伴生对象中通过广播变量获取值。这种方式能保证所有节点拿到同一个不可变实例,且避免了全局可变状态:object Demo { private val demoParamsBroadcast = SparkContext.getOrCreate().broadcast(DemoParams("Salmon")) val demoParams: DemoParams = demoParamsBroadcast.value // 其他代码... } - 构造参数传递:虽然你想避免传参,但这是分布式环境下最安全的方式——每个
Demo实例携带自己的DemoParams,不依赖全局静态变量,彻底规避节点间状态不一致的问题:class Demo(demoParams: DemoParams) { def foo = { println("Demo params name is: ", demoParams.name) anotherFoo(demoParams.name) } // 其他代码... } - lazy val初始化:如果参数只能在运行时确定,可使用
lazy val延迟初始化,但必须确保所有Executor节点都能执行初始化逻辑(比如将初始化代码放在Driver端广播,或确保节点能获取到初始化所需的资源):object Demo { lazy val demoParams: DemoParams = { // 这里写运行时初始化逻辑,比如从配置文件读取 DemoParams("Salmon") } // 其他代码... }
问题2:NPE产生的原因
Spark集群中,Driver和每个Executor是完全独立的JVM进程:
- 你在Driver端的
run方法中给Demo.demoParams赋值,这个操作仅在Driver的JVM中生效; - 当
Demo类的实例被序列化并发送到Executor节点执行foo方法时,Executor节点的Demo伴生对象会重新初始化,此时demoParams还是初始的null; - 你看到
println能成功输出,大概率是因为foo方法的这部分代码在Driver端执行(比如未触发分布式计算操作),但anotherFoo的调用被分发到Executor执行,此时访问Demo.demoParams.name就会因demoParams为null抛出NPE。
内容的提问来源于stack exchange,提问作者smaug
相关产品推荐
相关产品推荐

