为什么在方法调用期间清空变量会使传递给方法的值也为 null?



我对一些Java行为感到非常困惑,特别是因为我之前对java的理解是它是严格按值传递的。

如果我将一个对象传递给一个方法,然后在调用线程中将该对象为 null,我希望该对象仍然存在并且能够在方法中进行操作,但事实并非如此。

下面是一些示例代码:

public class methodTest {
boolean bool;
boolean secondBool;
Test    list;
public static void main(String[] args) {
new methodTest().run();
}
public void run() {
list = new Test();
new Thread(() -> {
func(list);
}).start();
list = null;
bool = true;
while (!secondBool) {
// block
}
System.gc();
}
public void func(Test big) {
while (!bool) {
// block
}
System.out.println(big);
secondBool = true;
}
class Test {
@Override
protected void finalize() throws Throwable {
System.out.println("I'm getting cleaned up!");
}
@Override
public String toString() {
return "Test!";
}
}
}

此代码打印

null
I'm getting cleaned up!

当我期望它打印时:

Test!
I'm getting cleaned up!

我在末尾包含了垃圾收集调用,并在类中包含 finalize 方法,以确保 gc 在函数可以打印它之前不会到达变量list,但没有它也会发生相同的效果。

这种行为的解释是什么?

编辑:

另一个示例是,如果您将 run 方法中的list = null行更改为list = new Test(),然后稍微修改toString以计算Test实例的数量。程序将打印"Test2"而不是"Test1",因为func中的参数值在调用线程中被覆盖,即使根据我对java按值传递系统的理解,这不应该发生。

它打印 null 是因为存在争用条件,并且新线程大部分时间都会丢失。

时机是这样的:

  1. 主线程(运行methodTest.run()的线程(创建一个新的Thread对象并启动新线程(这意味着它会创建一个新的Linux/Windows线程并通知它可以开始运行(
  2. 下一步,它将实例变量list设置为 null
  3. 与上面的步骤 2 并行,第二个线程开始运行并最终到达 lambda() -> { func(list); }
  4. 只有当第二个线程执行func(list);时,它才会读取实例变量list

因此,作为参数big传递到func的内容完全取决于

  • 主线程需要多少时间才能执行list = null;
  • 操作系统需要多少时间才能开始执行第二个线程,以及第二个线程到达它调用func(list);的点

根据你的观察,第二个线程需要比主线程需要执行func(list);的时间更多的时间list = null;

如果我没记错的话,它会更改用户定义类的值。因此,如果您使用原始数据类型和已经定义的类(如 int、字符串等(,那么它不会给您带来此问题。但是在这里你使用的是你自己的类,所以出于某种原因它通过引用传递。

最新更新