正在使用" final" Java中的参数线程安全

时间:2017-03-04 02:57:50

标签: 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并且obj被缓存直到线程中需要。

但是如果有一连串调用doSomethingAsync以便在线程使用obj执行任何操作之前完成调用,会发生什么?

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

3 个答案:

答案 0 :(得分:2)

乍一看似乎很好。调用者调用doSomethingAsync并在线程中需要缓存obj。

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

但是如果有一连串调用doSomethingAsync以便它们在线程用obj完成任何操作之前完成会发生什么呢?

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

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

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

答案 1 :(得分:0)

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

看来Peri真正的问题是关于Java存储局部变量的方式,如“obj”供Thread使用。如果您想自己谷歌,这称为“捕获变量”。有一个很好的讨论here

基本上发生的是当实例化本地类时,所有局部变量,存储在堆栈中的变量以及“this”指针都会被复制到本地类(本例中为Thread)。

为了评论,原来的答案如下。但它现在已经过时了。

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

同样,如果使用相同的对象调用doSomethignAsync两次,那么您不知道哪个异步线程会首先修改对象,并且不保证它们不会同时对其进行操作。相同的对象。

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

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

答案 2 :(得分:0)

  

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

不,这不会发生。随后对doSomethingAsync的调用无法覆盖之前obj调用所捕获的doSomethingAsync。即使您删除了最后一个关键字,也会这样(假设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();
}

现在,假设您创建了两个YourThread实例,第二个实例如何修改作为参数传递给第一个实例的内容

关闭其他语言

在其他语言中,神奇的东西确实可以发生,但为了表明它你需要编写略有不同的代码:

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视为null

类似地,在下面的代码中(同样,在Java中无效),如果thread2首先执行并且在obj方法中更改run,则thread2可能会影响thread1:

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 ();
}

返回Java

Java强迫您在final上放置obj的原因是虽然Java的语法看起来与其他语言中使用的闭包语法非常相似,但它没有执行相同的闭包语义。知道它是最终的,Java不需要创建捕获对象(因此额外的堆分配),但在场景后面使用类似于YourThread的东西。有关详细信息,请参阅此link