简体   繁体   English

在Java中使用“最终”参数线程安全

[英]Is using “final” parameter thread safe in Java

In this example, is it sufficient to declare the parameter obj as final to safely use it in the thread, below? 在此示例中,将参数obj声明为final足以在下面的线程中安全使用它是否足够?

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

  thread.start ();
}

At first glance it may seem fine. 乍看起来似乎还不错。 A caller invokes doSomethingAsync and obj gets cached until needed in the thread. 调用方调用doSomethingAsync并且obj会被缓存,直到线程中需要为止。

But what happens if there are a burst of calls to doSomethingAsync such that they complete before the threads have done anything with obj ? 但是,如果突然出现对doSomethingAsync的调用,使它们在线程对obj任何操作之前完成,该怎么办?

If the Java compiler simply makes obj into a member variable, the last call to doSomethingAsync will overwrite the prior values of obj , making prior invocations of the thread use a wrong value. 如果Java编译器只是简单地使obj成为成员变量,则对doSomethingAsync的最后一次调用将覆盖obj的先前值,从而使线程的先前调用使用错误的值。 Or, does the compiler generate a queue or some dimensioned storage for obj so that each thread gets the proper value? 或者,编译器是否为obj生成队列或某种尺寸的存储,以便每个线程都获得正确的值?

At first glance it may seem fine. 乍看起来似乎还不错。 A caller invokes doSomethingAsync and obj gets cached until needed in the thread. 调用方调用doSomethingAsync,并且obj会被缓存,直到线程中需要为止。

The object is not "cached", the variable reference merely cannot be assigned to another object. 该对象不是“缓存的”,变量引用仅不能分配给另一个对象。 The final keyword only prevents the variable from being re-assigned, it does not prevent the object that is being referenced from being mutated. final关键字仅防止重新分配变量,而不能防止所引用的对象发生突变。

But what happens if there are a burst of calls to doSomethingAsync such that they complete before the threads have done anything with obj? 但是,如果突然出现对doSomethingAsync的调用,使它们在线程对obj执行任何操作之前完成,该怎么办?

If the threads modify the referenced object the behavior would be undefined, they would be competing for the object and their reference to the object may have "old" values because the object was not synchronized between the threads. 如果线程修改了引用的对象,则行为将是不确定的,它们将争夺该对象,并且由于对该对象在线程之间未同步,因此对对象的引用可能具有“旧”值。 If the object is immutable, it has no state and cannot be changed, then it is inherently thread safe. 如果对象是不可变的,则它没有状态并且不能更改,那么它本质上是线程安全的。

If the Java compiler simply makes obj into a method variable, the last call to doSomethingAsync will overwrite the prior values of obj, making prior invocations of the thread use a wrong value. 如果Java编译器只是将obj变成方法变量,则对doSomethingAsync的最后一次调用将覆盖obj的先前值,从而使线程的先前调用使用错误的值。 Or, does the compiler generate a queue or some dimensioned storage for obj so that each thread gets the proper value? 或者,编译器是否为obj生成队列或某种尺寸的存储,以便每个线程都获得正确的值?

The compiler does not guarantee that the threads get executed in order, threads run concurrently. 编译器不保证线程按顺序执行,线程并发运行。 This is why the synchronize keyword exists, so that you can guarantee that when you reference the object you reference the same state of the object that all of the other threads see. 这就是为什么存在synchronize关键字的原因,因此可以保证在引用对象时,引用的是所有其他线程看到的对象的相同状态。 Obviously this is at a cost to performance so it is recommended to only pass immutable objects into threads so that you don't have to synchronize the threads every time you do something with the object. 显然,这是以性能为代价的,因此建议仅将不可变的对象传递到线程中,这样就不必在每次对对象执行操作时都同步线程。

Large edit here, based on a conversation the Original Poster and I had in chat. 在此根据我和原始聊天者的交谈进行大量编辑。

It seems Peri's real question was about the way Java stored local variables like "obj" for use by Thread. 似乎Peri的真正问题是Java存储本地变量(如“ obj”)供Thread使用的方式。 This is called "captured variables" if you want to google it yourself. 如果您想自己用Google搜索它,则称为“捕获变量”。 There is a nice discussion here . 有一个很好的讨论在这里

Basically what happens is that all your local variable, the ones stored on the stack, plus the "this" pointer get copied into your local class (Thread in this case) when the local class is instantiated. 基本上发生的是,实例化本地类时,所有本地变量,存储在堆栈中的那些本地变量以及“ this”指针都被复制到您的本地类(在本例中为Thread)中。

Original answer follows for the sake of the comments. 为了评论起见,原始答案如下。 But it is now obsolete. 但是现在已经过时了。

Each time you call doSomethingAsync you are creating a new thread. 每次调用doSomethingAsync您都在创建一个新线程。 If you call doSomethingAsync just once with a particular object, and then you modify that same object in the calling thread, then you have no idea what what the asynchronous thread will do. 如果只对特定对象调用一次doSomethingAsync ,然后在调用线程中修改该对象,那么您将不知道异步线程将做什么。 It might "do something with the object" before you modify it in the calling thread, after you modify in the calling thread or even WHILE you are concurrently modifying it in the calling thread. 在调用线程中对其进行修改之前,或者在调用线程中进行修改之后,甚至可能同时在调用线程中对其进行修改时,它可能都会“对该对象执行某些操作”。 Unless the Object itself is thread safe this will cause problems. 除非对象本身是线程安全的,否则将导致问题。

Similarly, if you call doSomethignAsync twice with the same object, then you have no idea which asynchronous thread will modify the object first, and no guarantee they will not act concurrently on the same object. 同样,如果您对同一个对象调用doSomethignAsync两次,那么您将不知道哪个异步线程会首先修改该对象,也无法保证它们不会同时作用于同一个对象。

Finally, if you call doSomethignAsync twice with 2 different objects then you don't know which asynchronous thread will act on its own object first, but you don't care, because they can't conflict with each other unless the objects have Static mutable variables (class variables) that are being modified). 最后,如果您使用2个不同的对象两次调用doSomethignAsync ,则您不知道哪个异步线程首先对自己的对象起作用,但是您不在乎,因为除非对象具有静态可变对象,否则它们不会相互冲突。变量(类变量)。

If you require that one task get completed before another task and in the order submitted, then a single threaded ExecutorService is your answer. 如果您要求一项任务先于另一项任务并按提交的顺序完成,那么单线程ExecutorService就是您的答案。

If the Java compiler simply makes obj into a member variable, the last call to doSomethingAsync will overwrite the prior values of obj, making prior invocations of the thread use a wrong value 如果Java编译器只是将obj变成成员变量,则对doSomethingAsync的最后一次调用将覆盖obj的先前值,从而使线程的先前调用使用错误的值

No, this will not happen. 不,这不会发生。 The subsequent call to doSomethingAsync cannot overwrite the obj captured by previous invocations of doSomethingAsync . 随后对doSomethingAsync调用不能覆盖由doSomethingAsync的先前调用捕获的obj This stands even if you remove the final keyword (assume java let you do it for just this time). 即使您删除了final关键字,这仍然成立(假设Java允许您暂时这样做)。

I think your question ultimately is about how closure works/is implemented in java. 我认为您的问题最终是关于闭包如何在Java中实现/实现的。 However, your code is not demonstrating the complication in the proper way because the code is not even trying to modify the variable obj in the same lexical scope . 但是,您的代码没有以正确的方式演示复杂性,因为代码甚至没有尝试在相同的词法范围内修改变量obj

In a way Java is not really capturing the variable obj , but its value . 在某种程度上,Java并不是真正地捕获变量 obj ,而是其 You could write the your code in a different way, and the overall effect is the same: 您可以用不同的方式编写代码,并且总体效果是相同的:

class YourThread extends Thread {
    private Object param;

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

    @Override
    public void run(){
        //do something with your param
    }
}

and you no longer need the final keyword: 并且您不再需要final关键字:

public void doSomethingAsync (Object obj){
    Thread t = new YourThread (obj);
    t.start();
}

Now, say you have two instances of YourThread created, how could the second instance modify what has been passed as parameter to the first instance ? 现在,假设您已经创建了YourThread的两个实例, 第二个实例如何修改已作为参数传递给第一个实例的内容


Closure in Other Languages 封闭其他语言

In other languages, magical things can indeed happen, but to show it you need to write the code slightly different: 在其他语言中,确实可以发生神奇的事情,但是要显示出来,您需要编写略有不同的代码:

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

This is not valid Java code, but in certain languages code like that is allowed. 这不是有效的Java代码,但在某些语言中,允许使用类似的代码。 And the thread, when its run method is executed, might see obj as null . 执行该线程的run方法时,它可能会将obj视为null

Similarly, in the below code (again, not valid in Java), thread2 could potentially impact thread1 if thread2 executes first and changes obj in its run method: 同样,在以下代码中(同样,在Java中无效),如果线程2首先执行并在其run方法中更改obj ,则线程2可能会影响线程1:

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

Back to Java 回到Java

The reason Java forces you to put a final on obj is that although Java's syntax looks extremely similar to the closure syntax used in other languages, it is not doing the same closure semantics. Java迫使您对obj进行final的原因是,尽管Java的语法看上去与其他语言中使用的闭包语法极为相似,但它并未执行相同的闭包语义。 Knowing it is final, Java does not need to create capturing object (thus additional heap allocation), but use something similar to YourThread behind the scene. 知道它是最终的,Java不需要创建捕获对象(因此不需要额外的堆分配),而是在幕后使用类似于YourThread的方法。 See this link for more details 请参阅此链接以获取更多详细信息

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM