正在使用;“最后的”;Java中的参数线程安全

正在使用;“最后的”;Java中的参数线程安全,java,thread-safety,Java,Thread Safety,在本例中,将参数obj声明为final是否足以在下面的线程中安全使用它 public void doSomethingAsync (final Object obj) { Thread thread = new Thread () { @Override public void run () { ... do something with obj ... } } thread.start (); } 乍一看似乎还不错。调用者调用doSomethingAsync并将ob

在本例中,将参数
obj
声明为final是否足以在下面的线程中安全使用它

public void doSomethingAsync (final Object obj)
{
  Thread thread = new Thread ()
  {
    @Override public void run () { ... do something with obj ... }
  }

  thread.start ();
}
乍一看似乎还不错。调用者调用
doSomethingAsync
并将
obj
缓存到线程中需要时

但是,如果对
doSomethingAsync
进行了大量调用,使得这些调用在线程对
obj
执行任何操作之前完成,会发生什么情况呢


如果Java编译器只是将obj变成一个成员变量,那么对
doSomethingAsync
的最后一次调用将覆盖
obj
的先前值,从而使线程的先前调用使用错误的值。或者,编译器是否为
obj
生成一个队列或某个维度的存储,以便每个线程获得正确的值?

根据原始海报和我在聊天中的对话,在此处进行大编辑

Peri真正的问题似乎是关于Java存储局部变量(如“obj”)以供线程使用的方式。如果你想自己用谷歌搜索它,这叫做“捕获变量”。有一个很好的讨论

基本上,当本地类被实例化时,所有本地变量(存储在堆栈上的变量)加上“this”指针都被复制到本地类(本例中为线程)中

为了便于评论,下面是原始答案。但现在它已经过时了

每次调用
doSomethingAsync
都是在创建一个新线程。如果对特定对象只调用一次
doSomethingAsync
,然后在调用线程中修改同一对象,则不知道异步线程将执行什么操作。在调用线程中修改对象之前、在调用线程中修改之后,甚至在调用线程中同时修改对象时,它可能会“对对象执行某些操作”。除非对象本身是线程安全的,否则这将导致问题

类似地,如果对同一对象调用两次
doSomethignAsync
,则不知道哪个异步线程将首先修改该对象,也不能保证它们不会同时作用于同一对象

最后,如果使用两个不同的对象调用
doSomethignAsync
两次,那么您不知道哪个异步线程将首先作用于自己的对象,但您不在乎,因为它们不会相互冲突,除非对象具有正在修改的静态可变变量(类变量)

如果您要求一个任务在另一个任务之前完成,并且按照提交的顺序完成,那么单线程ExecutorService就是您的答案。

乍一看,这似乎很好。调用方调用doSomethingAsync,obj被缓存,直到线程中需要为止

对象未被“缓存”,变量引用仅不能分配给另一个对象。
final
关键字只能防止变量被重新分配,但不能防止被引用的对象发生变异

但是,如果对doSomethingAsync进行了大量调用,使得它们在线程对obj执行任何操作之前完成,那么会发生什么呢

如果线程修改被引用的对象,那么行为将是未定义的,它们将竞争该对象,并且它们对该对象的引用可能具有“旧”值,因为该对象在线程之间不同步。如果对象是不可变的,它没有状态并且不能更改,那么它本质上是线程安全的

如果Java编译器只是将obj变成一个方法变量,那么对doSomethingAsync的最后一次调用将覆盖obj的先前值,从而使线程的先前调用使用错误的值。或者,编译器是否为obj生成一个队列或某个维度的存储,以便每个线程获得正确的值

编译器不保证线程按顺序执行,线程并行运行。这就是为什么
synchronize
关键字存在的原因,这样您就可以保证当您引用对象时,您引用的对象的状态与所有其他线程看到的相同。显然,这是以性能为代价的,因此建议只将不可变对象传递到线程中,这样就不必在每次对对象执行操作时同步线程

如果Java编译器只是将obj变成一个成员变量,那么对doSomethingAsync的最后一次调用将覆盖obj的先前值,从而使线程的先前调用使用错误的值

不,这不会发生。对
doSomethingAsync
的后续调用无法覆盖先前调用
doSomethingAsync
捕获的
obj
。即使您删除了最后一个关键字(假设java允许您在这一次这么做),这仍然有效

我认为您的问题最终是关于闭包是如何在java中实现的。但是,您的代码没有以正确的方式演示复杂性,因为代码甚至没有尝试在相同的词法范围内修改变量
obj

在某种程度上,Java并没有真正捕获变量
obj
,而是它的。您可以用不同的方式编写代码,总体效果相同:

class YourThread extends Thread {
    private Object param;

    public YourThread (Object obj){
        param = obj;
    }

    @Override
    public void run(){
        //do something with your param
    }
}
您不再需要最后一个关键字:

public void doSomethingAsync (Object obj){
    Thread t = new YourThread (obj);
    t.start();
}
现在,假设您创建了两个线程实例,第二个实例如何修改作为参数传递给第一个实例的内容


其他语言的闭包

在其他语言中,神奇的事情确实会发生,但要表现出来,您需要编写稍微不同的代码:

public void doSomethingAsync (Object obj){
    //Here let's assume obj is not null
    Thread thread = new Thread (){
        @Override 
        public void run () { ... /*do something with obj*/ ... }
    }

    thread.start ();
    obj = null;
}
这不是有效的Java代码,但在某些语言中,这样的代码是允许的。当执行
run
方法时,线程可能会将
obj
视为
n
public void doSomethingAsync (Object obj){

    Thread thread1 = new Thread (){
        @Override 
        public void run () { ... /*do something with obj*/ ... }
    }

    thread1.start ();

    Thread thread2 = new Thread (){
        @Override 
        public void run () { ... /*do something with obj*/ ... }
    }

    thread2.start ();
}