我对一些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 是因为存在争用条件,并且新线程大部分时间都会丢失。
时机是这样的:
- 主线程(运行
methodTest.run()
的线程(创建一个新的Thread
对象并启动新线程(这意味着它会创建一个新的Linux/Windows线程并通知它可以开始运行( - 下一步,它将实例变量
list
设置为 null - 与上面的步骤 2 并行,第二个线程开始运行并最终到达 lambda
() -> { func(list); }
- 只有当第二个线程执行
func(list);
时,它才会读取实例变量list
因此,作为参数big
传递到func
的内容完全取决于
- 主线程需要多少时间才能执行
list = null;
- 操作系统需要多少时间才能开始执行第二个线程,以及第二个线程到达它调用
func(list);
的点
根据你的观察,第二个线程需要比主线程需要执行func(list);
的时间更多的时间list = null;
如果我没记错的话,它会更改用户定义类的值。因此,如果您使用原始数据类型和已经定义的类(如 int、字符串等(,那么它不会给您带来此问题。但是在这里你使用的是你自己的类,所以出于某种原因它通过引用传递。